> ## 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.

# Anchor

> Use Agentcard with Anchor to let your agent make purchases with your users' cards.

Let an agent pay with your users' own cards while it shops in an [Anchor](https://anchorbrowser.io) browser. The agent types a placeholder card at checkout. The Agentcard SDK pauses the payment until the user approves it with their passkey, and the user's device then pays with their real card. Your own agent or Anchor's agent can do the shopping:

| | Your agent shops | Anchor's agent shops |
| - | - | - |
| You create | A session, with `createBrowser()` | A session, then a task in it with `agentTask()` |
| The SDK attaches to | The session's first page | Every page in the session, new tabs included, before the task starts |

Before you start, you need a user who has added a card to the Vault, and the `user_id` you received when they added it. See [Adding a card](/vault/adding-a-card).

## Install

```bash theme={null}
npm i @agent-cards/sdk anchorbrowser playwright
```

`anchorbrowser` reads your Anchor API key from `ANCHORBROWSER_API_KEY`. The samples read your Agentcard client id and secret from `AGENTCARD_CLIENT_ID` and `AGENTCARD_CLIENT_SECRET`. Create them in the [dashboard](https://app.agentcard.sh), as step 1 of the [Vault Quickstart](/vault/quickstart) shows.

## Shop with your own agent

Create an Anchor session, and attach the SDK to its page before your agent reaches the payment form.

```ts theme={null}
import { createBrowser } from 'anchorbrowser';
import { VaultClient, attachToPlaywright } from '@agent-cards/sdk';

const { browser, session } = await createBrowser();
const sessionId = session.data?.id;
if (!sessionId) throw new Error('Anchor returned no session id');
const page = browser.contexts()[0].pages()[0];

const vault = new VaultClient({
  clientId: process.env.AGENTCARD_CLIENT_ID!,
  clientSecret: process.env.AGENTCARD_CLIENT_SECRET!,
});
await vault.syncRegistry();

const checkout = await attachToPlaywright(page, {
  vault,
  user: 'usr_8f3k2m',
  merchant: 'shop.agentcard.sh',
  amount: 583,
  currency: 'usd',
  onApprovalUrl: (url) => sendToUser(url),
});

// Now let the agent shop on this page.
await page.goto('https://shop.agentcard.sh');
```

| Option | What to pass |
| - | - |
| `user` | The `user_id` of the user whose card pays. |
| `merchant` | The store's domain. `shop.agentcard.sh` is Agentcard's demo store on Stripe test mode. |
| `amount`, `currency` | The total the user approves, in cents: `583` is \$5.83. |
| `onApprovalUrl` | Your own function that gets the approval link to the user. `sendToUser` stands in for it. |

`syncRegistry()` loads the list of payment processors the SDK can pause. Call it once after you create the client.

Have your agent type any of Stripe's published test card numbers as the placeholder. The SDK pauses the payment and calls `onApprovalUrl` with a link to the user's approval screen. Send the link from your own app, such as a push notification or an in-app message, and keep it out of your logs: the link opens the approval screen for this payment, and only the user should see it.

If your agent opens the checkout in a new tab, attach to that page too, as the next section shows.

### Fix an attach error

Anchor runs its agent and its blockers as browser extensions in the session. `@agent-cards/checkout` up to 0.19.0, the SDK's older name, refuses a page next to them, and `attachToPlaywright` throws:

```text theme={null}
Service workers are active; use a checkout context created with serviceWorkers: "block".
```

Install `@agent-cards/sdk` 0.20.0 or later instead of creating the context the error asks for. A new context has none of the session's saved logins.

```bash theme={null}
npm i @agent-cards/sdk@latest
```

## Shop with Anchor's agent

Refuse card requests from any tab the SDK has not attached to, attach the SDK to every page in the session, then pass `sessionId` to `agentTask`, so that Anchor's agent shops in that same session. The sample reuses `vault` and the imports from the previous section.

A task without `sessionId` runs in a new browser where the SDK is not attached. The store receives the placeholder card, and the user's card never pays.

```ts theme={null}
import { createBrowser, agentTask, Sessions } from 'anchorbrowser';
import type { Page } from 'playwright';

const { browser, session } = await createBrowser();
const sessionId = session.data?.id;
if (!sessionId) throw new Error('Anchor returned no session id');
const context = browser.contexts()[0];

// A tab the SDK has not attached to yet refuses card requests. Once the SDK
// is attached to a tab, its own route decides for that tab.
const attached = new WeakSet<Page>();
await context.route(
  (url) => vault.isCardRequest(url.toString()),
  (route) => {
    const request = route.request();
    const page = request.serviceWorker() ? null : request.frame().page();
    const held = page !== null && attached.has(page);
    return !held && vault.isCardRequest(request.url(), request.method()) ? route.abort() : route.fallback();
  },
);

// Attach the SDK to every page, and wait until it is attached.
const attach = (page: Page) =>
  attachToPlaywright(page, {
    vault,
    user: 'usr_8f3k2m',
    merchant: 'shop.agentcard.sh',
    amount: 583,
    currency: 'usd',
    onApprovalUrl: (url) => sendToUser(url),
  }).then((checkout) => {
    attached.add(page);
    return checkout;
  });
const checkouts = await Promise.all(context.pages().map(attach));
// The agent can open the checkout in a new tab, so attach to every page it opens.
// A tab the SDK cannot attach to is closed, so the agent never pays in it.
context.on('page', (page) => {
  attach(page).then(
    (checkout) => checkouts.push(checkout),
    () => page.close().catch(console.error),
  );
});

const card = process.env.PLACEHOLDER_CARD_NUMBER!;
try {
  const result = await agentTask(
    `Buy one Enamel Camp Mug. Pay with card ${card}, expiry 12/34, CVC 123, and click Pay once. The payment then waits for the cardholder to approve on their phone. Do not click Pay again. Wait until the page shows the order is paid.`,
    { sessionId, taskOptions: { url: 'https://shop.agentcard.sh' } },
  );
  console.log(result.data.result);
  // Confirm the order with the merchant here, while the session is open.
} finally {
  // Log a failed cleanup instead of throwing it: thrown here, it would replace
  // the task's result, and a caller who retries would pay again.
  await Sessions.deleteSession({ path: { session_id: sessionId } }).catch(console.error);
}
```

The context route covers a tab the agent opens before the SDK has attached to it. A card request from that tab fails instead of reaching the store, so the agent cannot pay there without the user's approval. Once the SDK is attached to a tab, the route steps aside, and requests the SDK lets through after an approval reach the store.

Set `PLACEHOLDER_CARD_NUMBER` to any of Stripe's published test card numbers. Put the placeholder card in the task, and tell the agent that the payment waits for the user after it clicks Pay, so that the agent does not click Pay again while the user approves. Keep the approval link out of the task, because Anchor's agent reads the task.

The task returns the agent's own summary of what it did. This run against the demo store printed:

```text theme={null}
  Order paid successfully. One Enamel Camp Mug was purchased for $5.83. I clicked Pay only once and waited for the cardholder approval flow; the page now shows “Order paid ✓”.
```

## Confirm the order

The agent's summary is not proof that the merchant took the order. From here the purchase runs like any Vault purchase: confirm the order with the merchant before you tell the user it is done, as step 4 of [Completing a purchase](/vault/completing-a-purchase) shows.

## End the session

The session keeps running after you close the Playwright connection, until Anchor times it out. End it in a `finally` block once you have confirmed the order, so that a failed purchase does not leave it running either. Log a failed cleanup instead of throwing it, so that a paid order never reads as a failure that a retry would pay again. The second sample does both, and the first follows the same shape:

```ts theme={null}
import { Sessions } from 'anchorbrowser';

try {
  // Your agent shops on the page, and you confirm the order with the merchant.
} finally {
  await Sessions.deleteSession({ path: { session_id: sessionId } }).catch(console.error);
}
```


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