> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentcard.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Complete a purchase on a Shopify store with your own browser

> Learn how an agent in a browser you run yourself buys from a Shopify store and pays with the user's Vault card, and what changes when nobody hosts the browser for you.

This guide makes the same purchase as [Complete a purchase on a Shopify store with Kernel](/guides/complete-a-purchase-on-a-shopify-store-with-kernel), a \$0.99 PDF from a Shopify store paid with the user's Vault card, from the Google Chrome installed on the machine that runs the script. There is no browser provider in the loop. The browser is a process you own, which changes how you watch it, how long you keep it, and who gets the approval link. Every line of output below is from the run that produced order 33230218 on 2026-10-05, including the two runs before it that bought nothing.

[Your own browser](/vault/integrations/agent-browsers/your-own) shows how the SDK attaches to a Playwright page you launched, to a raw CDP connection, or to your own interception. This guide does not repeat it. It takes the Playwright level and runs the whole purchase through it. The shopping leg, the attach call, the placeholder card and the merchant result reader are the same as in the Kernel guide, so this page shows only what differs and links to that guide for the rest.

## Before you start

<Note>
  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.
</Note>

* `@agent-cards/sdk` and `playwright-core` installed, and a Chromium to launch: the Google Chrome on the machine (`BROWSER_CHANNEL=chrome`), or Playwright's own build after `npx playwright-core install chromium` (leave `BROWSER_CHANNEL` unset).
* An Agentcard organization with production credentials, and a user of that organization with a card in the Vault. [Adding a card](/vault/adding-a-card) gets you both.
* A way to reach the user: the approval link has to leave your process. The first run of this guide forgot that, and the section on delivering the link shows what that costs.
* The Kernel guide's steps 1 to 4 read once: [open the checkout](/guides/complete-a-purchase-on-a-shopify-store-with-kernel#1-open-the-checkout), [fill it and read the total](/guides/complete-a-purchase-on-a-shopify-store-with-kernel#2-fill-the-checkout-and-read-the-total), [attach](/guides/complete-a-purchase-on-a-shopify-store-with-kernel#3-attach-before-the-card-form), [pay](/guides/complete-a-purchase-on-a-shopify-store-with-kernel#4-pay-with-a-placeholder-card). They run unchanged here.

## Tools

<CardGroup cols={3}>
  <Card title="Playwright" href="https://playwright.dev" img="https://mintcdn.com/agentcard/K-bylD7afogWRSuH/images/logos/playwright.svg?fit=max&auto=format&n=K-bylD7afogWRSuH&q=85&s=9b4377712e33e7fe68ac15d9d29f24c2" width="64" height="64" data-path="images/logos/playwright.svg" />

  <Card title="Agentcard Vault" href="/vault/quickstart" img="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/logos/agentcard.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=65d1dac5a31a8025bfb126178b459052" width="512" height="512" data-path="images/logos/agentcard.png" />

  <Card title="Shopify checkout" href="https://www.shopify.com" img="https://mintcdn.com/agentcard/GVN3ZmhUebCC6YuZ/images/logos/shopify.svg?fit=max&auto=format&n=GVN3ZmhUebCC6YuZ&q=85&s=ca26e7d390455ac7601c66ec43058130" width="64" height="64" data-path="images/logos/shopify.svg" />
</CardGroup>

## What changes without a provider

| | Kernel browser | Your own browser |
| - | - | - |
| Start | `kernel.browsers.create()`, then connect over CDP | `chromium.launch()` |
| Watch it | The session's live view URL | `headless: false`, or screenshots |
| Where it exits | Kernel's proxy, a stealth profile | Your machine's IP, a plain Chrome |
| When it ends | You delete the session, or its idle timeout does | Your process ends, or you close the browser |
| Keep it after a pause | The session outlives your process | Your process has to stay alive |

The last row is the one that bites. With a hosted browser you can exit your script and come back to the session. Here the browser dies with the process, so a run that is waiting on an approval or on the merchant's answer has to keep running.

## 1. Launch the browser

```ts theme={null}
import { chromium } from 'playwright-core';

const channel = process.env.BROWSER_CHANNEL;   // 'chrome' for the Google Chrome on this machine; unset for Playwright's own Chromium
const browser = await chromium.launch({
  ...(channel ? { channel } : {}),
  headless: process.env.HEADED !== '1',         // HEADED=1 shows the window
});
const context = await browser.newContext({ serviceWorkers: 'block' });
const page = await context.newPage();
```

`serviceWorkers: 'block'` is as necessary here as on a hosted browser: a service worker owns requests the SDK cannot see, and Shopify's card request is one of them. Headless Chrome shopped and paid on this run without a bot check; a store that shows one is a reason to run headed, or to take the Kernel guide's path.

<img src="https://mintcdn.com/agentcard/K-bylD7afogWRSuH/images/guides/own-browser-shopify/01-product.png?fit=max&auto=format&n=K-bylD7afogWRSuH&q=85&s=9a16960e78596964d3717fb1c82b14d4" alt="The product page in headless Chrome: High School Biology Answer Key (PDF), 99 cents, Add to Cart" width="1280" height="900" data-path="images/guides/own-browser-shopify/01-product.png" />

## 2. Shop, attach, and pay as in the Kernel guide

Steps 1 to 4 of the Kernel guide run here line for line: product page, cart, the cookie notice, the checkout form, the total read once it settles, `attachToPlaywright` with the same options, the placeholder card in Shopify's iframes, Pay now once. The companion script for this guide differs from the Kernel one in the launch above and in how it closes the browser below, and nowhere else.

<img src="https://mintcdn.com/agentcard/K-bylD7afogWRSuH/images/guides/own-browser-shopify/03-checkout-filled.png?fit=max&auto=format&n=K-bylD7afogWRSuH&q=85&s=f68ed9f2d10c8e39ca3c2f5f7771cf52" alt="The checkout filled in from headless Chrome: billing address and the order summary at 99 cents" width="1280" height="900" data-path="images/guides/own-browser-shopify/03-checkout-filled.png" />

<img src="https://mintcdn.com/agentcard/K-bylD7afogWRSuH/images/guides/own-browser-shopify/04-placeholder-card.png?fit=max&auto=format&n=K-bylD7afogWRSuH&q=85&s=add84109661c1e913ef4f14ca75860df" alt="Shopify's card fields filled with the placeholder card, Pay now below" width="1280" height="900" data-path="images/guides/own-browser-shopify/04-placeholder-card.png" />

## 3. Deliver the approval link

`onApprovalUrl` hands your code the link. Nothing happens until a person opens it. The first 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:

```text theme={null}
18:00:14 state=awaiting_approval auth=cauth_73026e5483c54cd777b83801
18:00:14 APPROVAL LINK (send it to the user, never to the agent): https://vault.agentcard.sh/authorize?id=cauth_73026e5483c54cd777b83801
18:15:13 state=outcome_unknown reason=authorization_poll_failed auth=cauth_73026e5483c54cd777b83801
18:15:13 event failed "PaymentOutcomeUnknownError"
```

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 second run delivered the link but the user was 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, such as the thread you have with them.

```ts theme={null}
import { execFile } from 'node:child_process';

onApprovalUrl: (url) => {
  if (process.env.APPROVAL_LINK_COMMAND) {
    execFile('/bin/sh', ['-c', process.env.APPROVAL_LINK_COMMAND], { env: { ...process.env, APPROVAL_URL: url } });
  }
},
```

```bash theme={null}
APPROVAL_LINK_COMMAND='./text-the-user.sh "Approve the purchase: $APPROVAL_URL"' npm run buy:local
```

The link opens only for the cardholder it is bound to; another account sees "That approval link is not valid for your account." It still goes to the user and nowhere else: not to the agent, not to a log the agent reads.

<img src="https://mintcdn.com/agentcard/K-bylD7afogWRSuH/images/guides/own-browser-shopify/05-after-pay-click.png?fit=max&auto=format&n=K-bylD7afogWRSuH&q=85&s=5577e3dc1b0069536e272d11c8ff888e" alt="The checkout after Pay now in headless Chrome: Processing while the card request is held" width="1280" height="900" data-path="images/guides/own-browser-shopify/05-after-pay-click.png" />

## 4. Expect Shopify to ask twice

On the run that bought the PDF, the user approved and the SDK replayed the real card. Three seconds later Shopify sent a second card request, and the SDK paused it and opened a second approval:

```text theme={null}
18:57:10 event card_request_paused {"url":"https://checkout.pci.shopifyinc.com/sessions","recognizer":"shopify"}
18:57:11 state=awaiting_approval auth=cauth_b8f26bf716be950c9bc57871
19:03:41 event authorized {"mode":"token","authorizationId":"cauth_b8f26bf716be950c9bc57871"}
19:03:41 state=awaiting_merchant auth=cauth_b8f26bf716be950c9bc57871
19:03:44 event card_request_paused {"url":"https://checkout.pci.shopifyinc.com/sessions","recognizer":"shopify"}
19:03:44 state=awaiting_approval auth=cauth_d51d76a81f267a76a9ac6d63
19:03:44 APPROVAL LINK (send it to the user, never to the agent): https://vault.agentcard.sh/authorize?id=cauth_d51d76a81f267a76a9ac6d63
19:07:38 event authorized {"mode":"token","authorizationId":"cauth_d51d76a81f267a76a9ac6d63"}
19:07:38 state=awaiting_merchant auth=cauth_d51d76a81f267a76a9ac6d63
```

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 store's order confirmation email arrived eight seconds later. The Kernel run of the day before completed on the first request; the difference is Shopify's, and your code cannot tell in advance which it gets.

<img src="https://mintcdn.com/agentcard/K-bylD7afogWRSuH/images/guides/own-browser-shopify/06-merchant-result.png?fit=max&auto=format&n=K-bylD7afogWRSuH&q=85&s=716b8d727eec4085cfd41fa6de1fdcbc" alt="Headless Chrome between the two approvals: the checkout still on Processing, the second card request held" width="1280" height="900" data-path="images/guides/own-browser-shopify/06-merchant-result.png" />

Three consequences for your code:

* 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 process alive through it. The wait in the Kernel guide's step 6 ended during this gap; the browser stayed open only because the script keeps it open while the merchant's answer is pending, and the second approval landed into that open page.

## 5. Read the merchant's record and close the browser

The checkout page is one witness; the store's own record is the one you report. Shopify sends two emails for a digital order, the order confirmation with the total and the card's last four digits, and the download notice with the order number, and the order status page says the same.

```text theme={null}
Subject: Thank You for Your Order            19:07:46
  Total $0.99 USD · Payment: Visa ending with 3132
Subject: Digital Downloads from The Good and the Beautiful   19:07:50
  Order 33230218
```

<img src="https://mintcdn.com/agentcard/K-bylD7afogWRSuH/images/guides/own-browser-shopify/07-order-status.png?fit=max&auto=format&n=K-bylD7afogWRSuH&q=85&s=12951a96608b1d0e76ad60283c25907e" alt="Shopify's order status page: Order 33230218, Confirmed Oct 5" width="1280" height="200" data-path="images/guides/own-browser-shopify/07-order-status.png" />

Close the browser when the merchant's record is in hand, or when the result is `failed` or `declined`. Until then keep it open and the process running, and keep asking: a `pending` answer has nothing else to read from, and the page is where the answer will appear. Three rules for the loop. A card request the user declined, or one that timed out, ends the run as that state, so read `getState()` first and stop on it. 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. And never stop the loop on a clock: a purchase that is still `pending` after an hour is still a purchase, and closing the browser then leaves it unreported. The companion script asks every fifteen seconds until the result settles, and a run that never clicked Pay closes the browser at once.

```ts theme={null}
const settled = (s) => ['completed', 'failed', 'declined'].includes(s.status);
if (paymentAttempted) {
  while (!settled(result)) {
    const state = checkout.getState();
    if (['declined', 'timed_out', 'cancelled', 'failed'].includes(state.status)) { result = state; break; }
    if (state.status === 'awaiting_approval') console.log('a new card request is waiting on the user');
    else {
      result = await checkout.reconcile().catch(() => result);
      console.log('merchant answer pending; browser kept open');
    }
    await page.waitForTimeout(15_000);
  }
  console.log(result.status, result.orderId ?? '', result.authorizationId ?? '');
}
await browser.close();
```

One order, one charge: two approvals for one checkout are two card tokens, and Shopify used the second. Confirm it the way this run did, from the store's order record, before you tell the user anything.

## Run it

```bash theme={null}
git clone https://github.com/tiny-agent-company/kernel-shopify-purchase
cd kernel-shopify-purchase && npm install
cp .env.example .env     # Agentcard credentials, user id, product, buyer, APPROVAL_LINK_COMMAND
set -a; source .env; set +a
npm run buy:local        # HEADED=1 npm run buy:local shows the window
```

Screenshots of every step land in `shots-local/`.

## Handle a run that stalls

| What you see | Why | What to do |
| - | - | - |
| `outcome_unknown` with `authorization_poll_failed` fifteen minutes after the click | Nobody opened the approval link | Deliver the link to the user from `onApprovalUrl`; the authorization reads `expired`, nothing was charged, run again |
| A second `awaiting_approval` right after `authorized` | Shopify asked for the card again | Deliver the new link, say it is the same purchase, click nothing |
| The process exits and the page is gone | Your script ended while the merchant's answer was pending | Keep reconciling until `completed`, `failed` or `declined`; read the store's record before deciding |
| A bot check or a blank checkout in headless Chrome | The store treats headless Chrome on your IP as a bot | Run with `HEADED=1`, or take the Kernel guide's path |

## Where to go next

* [Complete a purchase on a Shopify store with Kernel](/guides/complete-a-purchase-on-a-shopify-store-with-kernel) for the shopping leg, the attach options and Shopify's checkout in detail.
* [Your own browser](/vault/integrations/agent-browsers/your-own) for the raw CDP level and for doing your own interception.
* [Completing a purchase](/vault/completing-a-purchase) for the webhooks that tell your server the same story.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.