Skip to main content
This guide runs one real purchase end to end, once per agent browser the Vault integrates with: an agent puts a $0.99 PDF from a Shopify store in the cart, reaches the checkout, and pays with a card the user stored in the Vault. The user approves on their phone with Face ID, Shopify confirms the order, and the browser is ended. The shopping leg, the attach call, the placeholder card and the merchant result reader are the same in every browser; only how the browser starts, how you watch it and how it ends differ, and those parts are in tabs. Pick your browser in each tab group; the prose between the groups applies to all of them. Every screenshot and every line of output below comes from a run that bought the PDF: Kernel on 2026-10-04, Browserbase, Anchor and a local Chrome on 2026-10-05, and Browser Use on 2026-10-06. Each tab names its order. The integration pages (Kernel, Browserbase, Browser Use, Anchor, Your own browser) cover how each browser connects to the Vault and the attach snippet. This guide does not repeat them. It shows everything around that snippet: the shopping leg a Shopify store needs, what to pass when you attach, what the SDK reports while the user decides, how to read Shopify’s confirmation number, and how to leave nothing running. What you end up with: one script, BROWSER=<name> npm run buy, that prints completed order #XTLQVAF0Y and a browser you can watch while it happens.

Before you start

A real purchase needs production credentials. The sandbox recognises the card request and pauses it, but no approval link is sent and nothing is charged. Run the sandbox first to see the pause, then switch the credentials.
  • @agent-cards/sdk and playwright-core installed, plus the SDK of the browser you pick: @onkernel/sdk, @browserbasehq/sdk, browser-use-sdk or anchorbrowser, and that provider’s API key. Your own browser needs a Chromium to launch instead. @agent-cards/sdk is the SDK’s current name; @agent-cards/checkout still installs and re-exports it.
  • An Agentcard organization with production credentials (client_id and client_secret), and a user of that organization with a card in the Vault. Adding a card gets you both; the user’s user_id is what you pass to the SDK.
  • A Shopify store with guest checkout and something cheap. The runs below use The Good and the Beautiful, a homeschool publisher, and its $0.99 High School Biology Answer Key (PDF): a digital product, so nothing ships and the checkout asks for an email and a billing address only.
  • A way to reach the user, and their phone within reach. The approval link is theirs to open, and it expires after 15 minutes.

Tools

playwright

Agent browsers

agentcard

Agentcard Vault

shopify-bag

Shopify checkout

How it works

1. Start the browser

Each browser hands Playwright a page in a different way, and each one is watched and ended differently. Everything after this step takes browser and does not care where it came from.
Kernel creates a Chromium with a CDP address and a live view URL. The session outlives your process until you delete it or it idles out, which is what lets a run wait on the user.
Kernel's live view of the session while the approval is pending: the checkout page on Processing, the total 99 centsKernel’s browser_live_view_url opens the session in a tab. During the pause it shows the checkout exactly as the agent left it, which is what you show a user who asks what their agent is doing.

2. Open the checkout

Two things matter before the agent types anything. Create the context with service workers blocked, because a service worker owns requests Playwright cannot see, and the card request is one of them. And dismiss the store’s cookie notice yourself, because it sits over the checkout button.
The product page in the Kernel browser: High School Biology Answer Key (PDF), 99 cents, Add to Cart The cart: one item, 99 cents, with the store's cookie notice over the checkout button Shopify’s checkout URL is /checkouts/cn/<token>/en-us. A store with Shop Pay sends the cart permalink to shop.app first; the cart page’s own checkout button lands on the store’s checkout, which is where the SDK works.

3. Fill the checkout

The checkout is one page. Fill the contact email, choose the credit card payment method, and fill the billing address. Shopify recomputes tax for the address, so read the total after the address settles, not before: the estimate before the address said $1.06, and California charges no tax on a digital product, so the total the user approved was $0.99. The amount you pass is the amount the approval shows the user. Shopify’s card request carries no total of its own for the SDK to compare it against, so wait until the order summary has stopped changing before you read it, and read it again right before Pay now if anything on the page changed.
The checkout filled in from the Browserbase session: email, credit card selected, billing address, and the order summary with the total Use an email Shop Pay has never seen for the buyer, a fresh one per run. Shopify recognises an email it has seen and opens a prompt over the page, “Confirm it’s you” for an email with a Shop Pay account, or its own checkout modal for one that paid on a Shopify store before, and that prompt intercepts every click until it is closed. The first Anchor run of this guide reused the Browserbase run’s email and died on the card field:
Nothing was attached and nothing was charged. The second Anchor run used a new address and closed the prompt when it appeared; the companion script does both.

4. Attach before the card form

Attach once the billing address is in and before anything touches the card fields. The call is the one on your browser’s integration page, with the parts a real purchase needs: the total you just read, the time the user gets, and a merchant result reader.
sendToUser is yours: whatever already reaches the user, such as the thread your product has with them. The link opens only for the cardholder it is bound to, and it still goes to the user and nowhere else. Never hand it to the agent or log it where the agent reads. resolveMerchantResult reads the merchant’s answer, not the processor’s. Shopify’s thank-you page says Confirmation # and the number, then “You’ve paid for your order.” The resolver reports completed only with that number in hand; the thank-you URL alone, or the page before the number paints, is pending. The SDK never clicks Pay for you, and neither should a retry of yours. Leave requireMerchantResult off for a Shopify checkout. That option holds every further card request until you reconcile, and Shopify sends more of them right after the real card goes through; with the hold on, the SDK blocked eleven of them in two seconds and the page sat on “Processing…” until the browser was deleted. Nothing was charged, and nothing was bought either.

5. Pay with a placeholder card

Shopify renders each card field in its own iframe on checkout.pci.shopifyinc.com. The iframes are named card-fields-number-…, card-fields-expiry-…, card-fields-verification_value-… and card-fields-name-…; the suffix changes per page load, so match on the prefix. Each iframe holds a hidden honeypot input ahead of the real field; take the visible one, or the number lands in a field Shopify ignores. Type any of Stripe’s published test cards; the docs keep the number out of the page and the script reads it from PLACEHOLDER_CARD_NUMBER. The real card never enters the browser.
Shopify's card fields filled with the placeholder card, Pay now below Click Pay now once. Shopify posts the card to checkout.pci.shopifyinc.com/sessions, the SDK pauses that request, and onApprovalUrl fires with the link. The button reads “Processing…” and stays that way until the user decides; they have 15 minutes. The checkout after Pay now in the Browserbase session: the button reads Processing while the card request is held onApprovalUrl hands your code the link. Nothing happens until a person opens it. One run of this guide printed the link to the terminal and nothing else; nobody was reading the terminal, and this is what the SDK printed fifteen minutes later:
The authorization read expired afterwards, with no replay and nothing charged. The page sat on “Processing…” the whole time, and Shopify never saw a card. The SDK does not name that state expired: it reports outcome_unknown with the reason authorization_poll_failed, and the loop in step 8 has to confirm the expiry and end the run, or it polls a page that will never change. The first Browser Use run of this guide ended the same way with the link delivered but the user away from the phone; same output, same result. Make the delivery part of the run. The companion script runs a shell command with the link in APPROVAL_URL when APPROVAL_LINK_COMMAND is set; in your product it is whatever already reaches the user.

7. Expect a second approval

The user sees the merchant and the amount on the approval page and confirms with Face ID. Their device sends the real card to Shopify’s card session endpoint and reports the response; the SDK replays it into the paused request, and the checkout continues as if the browser had sent the real card. On the Kernel and Browser Use runs that was the end of it: on Kernel, eighteen seconds from click to approval, then three minutes of Shopify finishing the order; on Browser Use, the order confirmed thirteen seconds after the approval. On the Browserbase, Anchor and local Chrome runs Shopify asked for the card a second time, two seconds after the replay. This is the Browserbase run:
Shopify’s first request produced a card token the checkout did not turn into a payment, so the checkout asked for the card again. Each card request is its own authorization, so the user got a second link for the same purchase. They approved it, and the order completed twenty-three seconds later. The difference between one request and two is Shopify’s, and your code cannot tell in advance which it gets. Three consequences:
  • A second awaiting_approval after an authorized is not an error. Tell the user that the store asked again and that it is the same purchase, and deliver the new link the same way.
  • Do not click Pay again, and do not start a new checkout. The second request came from the page you already have.
  • Keep the browser through it. A hosted session stays up on its own; your own browser stays up only while your process does.
One order, one charge: two approvals for one checkout are two card tokens, and Shopify used the second. The store’s order confirmation email for the local Chrome run named one total, $0.99, and one card. The approval page is bound to the user the purchase was made for. If the user’s phone is signed in to the Vault as a different account, the page says “That approval link is not valid for your account.” The user signs out there, opens a Vault link your code made for their user_id, adds the card, and the approval link works.

8. Confirm the order

Once the state turns to awaiting_merchant, wait for the thank-you page and reconcile. End the browser when the result is completed, failed or declined, or when a card request the user declined or let expire ends the run. A pending or outcome_unknown result after a card was replayed means the merchant has not answered yet, and the page is where that answer appears, so the browser stays up and you reconcile again before you decide anything. Five rules for the loop:
  • Read getState() first and stop on declined, timed_out or cancelled.
  • Stop on an approval nobody gave, once the authorization confirms it. outcome_unknown with authorization_poll_failed while no card was ever replayed in this run means the SDK lost track of the approval; the reason is the same for an approval that expired and for a network error while the approval was still open. Retire the approval and read the authorization: expired or declined with replay_attempted: false ends the run with nothing charged. Any other answer is unknown, not expired: an authorization that reads approved, or expired with replay_attempted: true, means a card may have reached Shopify while the SDK was not listening. If an earlier card request in the same run was approved, do not stop there at all: Shopify has seen a card, so the outcome is the merchant’s to tell.
  • While a card request is paused on a new approval, the state is awaiting_approval and reconcile() has nothing to read yet, so wait rather than call it.
  • Never give up on a clock of your own: a purchase that is still pending after an hour is still a purchase.
  • A hosted session has a clock you do not own. Kernel’s ends at timeout_seconds of inactivity, Browserbase’s at timeout, Browser Use’s at timeout, Anchor’s at max_duration or idle_timeout. Set the limit well past the 15 minutes the user gets plus the minutes Shopify takes, and when the session ends anyway, stop with the purchase unknown and read the store’s record instead of the page.
An authorization that still reads awaiting_approval after the poll failed is a live link in the user’s hands with nothing listening for it. retireApproval cancels it and reads it back until Get an authorization no longer says awaiting_approval; a cancel that fails is retried. The attempt is bounded by the approval’s own life: an approval lives 15 minutes, so after 16 the link is dead whether or not the read ever answered, and a read that never answered leaves the result unknown. The run then goes on to the store’s record and to closing the browser instead of waiting on an API that is not answering. The status alone is not enough to call the run uncharged: replay_attempted says whether a device sent the card, and only false rules a charge out. On the first Browser Use run the read returned expired with replay_attempted: false, and on the run that bought the PDF approved with replay_attempted: true. findOrderInStoreRecord is yours, because the record is the store’s: Shopify emails an order confirmation to the buyer’s address within seconds of the charge, and the order status page says the same. The Anchor tab below is a run that needed it. Agentcard’s side of the story is the authorization: Get an authorization says whether the card was replayed and what the processor reported, and the checkout_authorization.expired webhook reports an approval nobody gave. Neither replaces the store’s record for whether an order exists. A purchase that is still unknown is reported to the user as unknown; a new checkout starts only after the store confirms there is no order. endBrowser is the one line that differs per browser, and each tab shows what its run printed at the end.
Shopify's thank-you page in the Kernel browser: Confirmation #5CQFGUXJX, You've paid for your order, and the PDF ready to download
A session you do not delete ends on its own at timeout_seconds of inactivity, which is the right thing when the merchant has not answered: the page is still there to read.
completed with a confirmation number is the only outcome you report as a purchase. pending and outcome_unknown mean keep the browser and look again; a missing receipt is not a failed payment, and clicking Pay again can charge twice. declined means the user said no or the processor refused; nothing was charged.

Run it

The whole run is one file in tiny-agent-company/kernel-shopify-purchase, the script every tab of this guide was captured from. BROWSER picks the browser; the rest of the script is shared.
The script delivers the approval link with APPROVAL_LINK_COMMAND and prints it; everything else is automatic, and a screenshot of every step lands in shots-<browser>/. HEADED=1 BROWSER=local npm run buy shows the local window.

Handle a run that stalls

The states in the table are the status values onStateChange prints, in the shape state=<status> reason=<reason>.

Where to go next