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

# Buy tool

> Builds the one tool that buys for one shopper, in the shape every agent framework takes.

`buyTool(options)` returns the `buy` tool for one shopper. The model calls it with the shopper's words, and Agentcard finds the products, builds the cart and places the order once the shopper approves. Each framework reads its own fields from the same object, so you pick no adapter.

```ts agent.ts theme={null}
import Anthropic from '@anthropic-ai/sdk';
import { AgentcardClient, buyTool } from '@agent-cards/commerce-sdk';

const agentcard = new AgentcardClient({
  clientId: process.env.AGENTCARD_CLIENT_ID ?? '',
  clientSecret: process.env.AGENTCARD_CLIENT_SECRET ?? '',
});

const buy = buyTool({
  agentcard,
  externalUserId: 'user-42', // your own id for the shopper
  sendLink: (link) => textShopper('user-42', link.url ?? ''),
});

const answer = await new Anthropic().beta.messages.toolRunner({
  model: 'claude-opus-5-5',
  max_tokens: 16000,
  tools: [buy],
  messages: [{ role: 'user', content: 'two packs of AA batteries from Amazon' }],
});
```

`textShopper` stands for your own code. The model calls `buy`, shows the shopper the cart, and confirms the cart only after the shopper says yes. Build the tool once per shopper and keep it. In a chat app, open a [turn](https://docs.agentcard.com/api-reference/commerce-sdk/turn.md) from it for each message.

## Create the tool

```ts theme={null}
buyTool(options: BuyToolOptions): BuyTool
```

Name your company's `AgentcardClient` and the shopper, or pass one shopper's own access token. The function throws a `TypeError` when you pass both `agentcard` and `accessToken`, or neither.

## Options

| Option | Type | Description |
| - | - | - |
| `agentcard` | `AgentcardClient`, required without `accessToken` | A client built with your company's credentials. Share one across every shopper's tool. |
| `accessToken` | `string`, required without `agentcard` | One shopper's own Agentcard access token, for a tool that buys only for them. |
| `externalUserId` | `string`, required with `agentcard` | Your own id for the shopper. Leave it out with `accessToken`. |
| `sendLink` | `(link: BuyToolLink) => void` or a promise, optional | Delivers a link to the shopper. Each call waits for it. A throw on a Vault, approval or store login link makes the call throw too. |
| `deliveryAddress` | `BuyDeliveryAddress`, optional | Where orders go, when you already hold the shopper's address. Agentcard never asks the shopper for it, and the model cannot change it. |
| `baseUrl` | `string`, optional | With `accessToken`: where Agentcard answers. Default `https://api.agentcard.sh`. |
| `fetch` | `FetchLike`, optional | With `accessToken`: the fetch to call Agentcard with. Default: the global `fetch`. |
| `wait` | `(ms: number) => Promise<void>`, optional | The pause between two reads of the conversation. Default: a timer. |
| `onReaction` | `(emoji: string) => void` or a promise, optional | Reacts to the shopper's message with `👀`, to show as a reaction on their message. A throw costs only the reaction. |
| `onWorking` | `(message: string) => void` or a promise, optional | Sends the shopper an instant text reply while Agentcard works. A throw costs only the reply. |
| `ack` | `'reaction'`, `'text'`, `'none'`, `false` or `(ask: string) => string`, optional | How the tool acknowledges a message. See [Acknowledgements](#acknowledgements). |
| `ackDelayMs` | `number`, optional | How long Agentcard has to answer before the reaction or instant reply goes out. Default `1500`. |
| `onResult` | `(result: BuyToolResult) => void` or a promise, optional | Hears the whole answer once per call, after its links went out and before the model reads it. |
| `onProgress` | `(progress: BuyProgress) => void`, optional | Hears each stage Agentcard reaches on a call: `searching`, `building_cart`, `checking_out` or `working`. |
| `stream` | `false`, optional | `false` waits for each whole answer. |
| `earlyCards` | `boolean`, optional | `true` sends the first product pages of a search to `sendLink` the moment the search answers. Default off. |
| `replyAsWritten` | `boolean`, optional | `true` sets `turn.replyAsWritten` on every turn this tool opens. Default off. |

`sendLink`, `onResult`, `onReaction` and `onWorking` hear every call the tool makes, whichever shopper message the call answers. To keep the links and answers of one message apart from those of another message, pass them to [`buy.turn`](https://docs.agentcard.com/api-reference/commerce-sdk/turn) instead.

A throw from `sendLink` on a product link leaves that product's `linkSent` false, and the call goes on. A throw from `onResult` costs only that call to it.

With `onProgress`, or with `earlyCards` and `sendLink`, the tool asks Agentcard to stream the call. Pass `stream: false` to wait for each whole answer.

### Acknowledgements

Left out, `ack` reacts through `onReaction` when the tool or the turn has one, and sends text through `onWorking` otherwise. `'text'` sends text even with an `onReaction`. `'none'` and `false` send nothing. A function writes the instant reply from the shopper's words, and an empty answer sends none.

The [acknowledging guide](https://docs.agentcard.com/commerce-sdk/guides/acknowledging-the-shopper.md) shows both modes.

## Members of the tool

| Member | Type | Description |
| - | - | - |
| `name` | `string` | The tool's name, `buy`. |
| `description` | `string` | The rules the model follows. Read it from `buy.description`. |
| `input_schema` | `BuyToolInputSchema` | The JSON Schema of the model's input, under Anthropic's field name. |
| `strict` | `true` | Asks the model API for input that always fits the schema. |
| `inputSchema` | `BuyToolStandardSchema` | The same schema as a Standard Schema, as the Vercel AI SDK reads it. |
| `openai` | `OpenAIBuyTool` | The tool in OpenAI's function-calling format. |
| `definition` | `BuyToolDefinition` | The name, description and input schema, for a framework with its own tool format. |
| `parse(input)` | `BuyToolInput` | Checks the model's input against the schema and throws a message the model can act on. |
| `run(input)` | `Promise<string>` | Runs one call and returns what the model reads, as JSON text. Anthropic's tool runner calls it. |
| `execute(input)` | `Promise<string>` | The same call, for the Vercel AI SDK. |
| `handle(args)` | `Promise<string>` | The same call for your own loop. Takes the model's arguments as an object or as their JSON text. |
| `answer(input)` | `Promise<BuyToolResult>` | Sends the model's input to Agentcard and returns the whole answer for your code. Never hand it to the model. |
| `forModel(result)` | `BuyToolModelView` | The answer as the model may read it, with every link taken out. |
| `instantAck(text)` | `string` or `null` | The instant text reply for a shopper's message, or `null` for a message that starts no search. |
| `instantReaction(text)` | `string` or `null` | The reaction for a shopper's message, `👀`, or `null`. |
| `turn(text, options)` | `BuyToolTurn` | Opens the turn of one shopper message. See [`buy.turn`](https://docs.agentcard.com/api-reference/commerce-sdk/turn). |
| `nextStep(message, options)` | `BuyToolTurn` or `null` | Opens the turn your agent takes once the shopper finishes a step outside the chat. |

`run`, `execute` and `handle` each return `JSON.stringify(buy.forModel(result))`. `handle` rejects a call with input outside the schema before anything reaches Agentcard. A call that fails rejects with a message that names no link, and `error.cause` keeps the original error for your logs. `answer` throws the `AgentcardApiError` itself.

Call `instantAck` or `instantReaction` the moment a message arrives, before your model runs. When either returns a reply, the tool sends none of its own for that turn, and the model reads `shopper_already_told_looking: true`.

## Frameworks

Pass `buy` as it is.

| Framework | Pass the tool | Who runs each call |
| - | - | - |
| Anthropic SDK, `client.beta.messages.toolRunner` | `tools: [buy]` | The tool runner. |
| Vercel AI SDK 7 | `tools: { buy }` | The SDK. |
| OpenAI SDK, OpenRouter's chat API, and any API that takes OpenAI's format | `tools: [buy.openai]` | Your loop, through `buy.handle`. |
| OpenRouter's agent SDK, `callModel` | `tools: [toOpenRouterTool(buy, { tool, z })]` | The SDK. |

`toOpenRouterTool(buy, { tool, z })` takes your own `tool` from `@openrouter/agent` and `z` from `zod`, so the package installs no dependencies. `toOpenRouterTool` accepts the tool or a turn. `toAnthropicTool(buy)` is deprecated and returns `buy` itself.

## The model's input

The model sends five fields and nothing else. The schema marks all five required and sets `additionalProperties` to `false`.

| Field | Type | Description |
| - | - | - |
| `ask` | `string` | What the shopper said, in their own words. |
| `confirm` | `boolean` | `true` only when the shopper said yes to the cart in the last result, asks to check out, says they added the card or approved, or asks for the approval link again. |
| `new_order` | `boolean` | `true` only when the shopper drops the cart to start a new order. |
| `product_id` | `string` or `null` | The `product_id` of the product the shopper picked from the last result's `products`. `null` when they picked none. |
| `store_id` | `string` or `null` | The `store_id` of the store the shopper picked from the last result's `stores`. `null` when they picked none. |

`parseBuyToolInput` accepts an object or its JSON text. The function trims `product_id` and `store_id`, reads an empty string as `null`, and throws a `TypeError` when both are set. Your own code may leave `product_id` and `store_id` out.

## BuyToolResult

`buy.answer` returns the whole answer for your code.

| Field | Type | Description |
| - | - | - |
| `status` | `BuyToolStatus` | Where the purchase stands. See [Status](#status). |
| `reply` | `string` | Agentcard's answer for the shopper, links included. |
| `messages` | `string[]` | The same answer as separate messages, cut at blank lines. |
| `cart` | `BuyCart` or `null` | The cart on the table. |
| `carts` | `BuyCart[]` | Every cart on the table, oldest first. |
| `products` | `BuyToolProduct[]` | The products the last search found. |
| `links` | `BuyToolLink[]` | The Vault, approval or store login link this answer needs. |
| `link_sent` | `boolean` | `true` when `sendLink` took every link in `links`. |
| `approval_expires_at` | `string` or `null` | When the approval link stops working, as an ISO 8601 timestamp. |
| `shopper_need_not_reply` | `boolean` | `true` when Agentcard tells the shopper how the order went. |
| `shopper_already_told_looking` | `boolean`, optional | `true` when this turn's reaction or instant reply reached the shopper. |
| `conversation_id` | `string` or `null` | The conversation. |
| `order_id` | `string` or `null` | The order this call placed. |
| `decline_code` | `string` or `null` | Why Agentcard did not buy. |
| `error_code` | `string` or `null` | A machine-readable failure. |
| `next_action` | `BuyNextAction` or `null` | The one step the shopper takes next. Only that step's link is in `links`. |
| `pick` | `BuyPick` or `null` | The product the model's `product_id` picked, and whether the cart now holds it. |
| `stores` | `BuyToolStore[]` | The stores the reply presents to choose from, at most eight. |
| `store_pick` | `BuyStorePick` or `null` | The store the call picked, and whether Agentcard selected it. |
| `hashes` | `string[]` | Every cart hash in this answer. |
| `cart_building` | `{ store, started_at }` or `null`, optional | The cart the store's checkout is still building. |
| `envelope` | `Record<string, unknown>` | Agentcard's answer as it came. |

### Status

| `status` | What happened |
| - | - |
| `needs_input` | Agentcard asks the shopper something, or shows a cart. |
| `needs_card` | The shopper adds a card from a link before Agentcard can buy. |
| `approval_sent` | The shopper approves the purchase from a link. |
| `placing` | The shopper approved, and the order is going through. |
| `order_placed` | The order went through. |
| `declined` | Agentcard did not buy, and `reply` says why. |

### Waits

A call that lands while Agentcard is still placing the order pauses once for 12 seconds and reads the conversation. A call that finds the order already placed reads it at once. A request that takes Agentcard more than 110 seconds comes back before its cart is ready, so the tool reads the conversation every 5 seconds for up to a minute and returns the cart once Agentcard finishes. Pass `wait` to replace the timer between reads.

## BuyToolLink

Each link has a `kind`. The fields below belong to the kinds named in the second column.

| Field | Kinds | Type | Description |
| - | - | - | - |
| `kind` | all | `'vault'`, `'approval'`, `'store_login'`, `'product'` or `'store'` | What the link is. |
| `url` | all | `string` or `null` | The page the link opens. `null` only on a `product` link for a product with no page, such as a flight option. |
| `expiresAt` | all | `string` or `null` | When the link stops working, as an ISO 8601 timestamp. Always `null` on a `product` link. |
| `id` | `vault`, `approval`, `store_login` | `string`, optional | A stable id, such as `vault_1x9k2qd`, with no part of the URL in it. |
| `store` | `store_login`, `approval` | `string` or `null`, optional | The store the sign-in is for, or the store whose cart the approval covers. |
| `title` | `product`, `store` | `string`, optional | The product's or store's name. |
| `action` | `product` | `'buy'`, `'reserve'` or `'book'`, optional | What taking the product does, so a button can say it. |
| `price` | `product` | `string` or `null`, optional | The price as the shopper reads it, such as `\$12.99`, or `from \$10.69` for a product priced by a required choice. |
| `productId` | `product` | `string`, optional | Agentcard's id for the product, with no link in it. Hand it back as `product_id` to pick the product. |
| `imageUrl` | `product`, `store` | `string` or `null`, optional | The picture. `null` when Agentcard sent none. |
| `images` | `product` | `string[]`, optional | Every photo, `imageUrl` first, at most eight. |
| `storeId` | `store` | `string`, optional | Agentcard's id for the store. Hand it back as `store_id` to pick the store. |
| `etaMinutes` | `store` | `number` or `null`, optional | The delivery estimate in minutes. |

The tool sends up to eight `product` links and eight `store` links per answer. The [Sending each link](https://docs.agentcard.com/commerce-sdk/guides/sending-each-link.md) guide shows how to render each kind.

## BuyToolProduct

| Field | Type | Description |
| - | - | - |
| `id` | `string` | The product's id. |
| `ref` | `string` or `null` | Agentcard's link-free id, which the model passes as `product_id`. |
| `name` | `string` | The product's name. |
| `action` | `'buy'`, `'reserve'` or `'book'` | What taking the product does. |
| `priceCents` | `number` or `null` | The price in the smallest unit of `currency`. |
| `priceFrom` | `true`, optional | Present when `priceCents` is the lowest price of a product priced by a required choice. |
| `currency` | `string` or `null` | The ISO 4217 code. |
| `imageUrl` | `string` or `null` | The main photo. |
| `images` | `string[]` | Every photo, `imageUrl` first. Empty when the product has none. |
| `store` | `string` or `null` | The store that sells it. |
| `url` | `string` or `null` | The product's page at its store. |
| `flight` | `BuyFlightItinerary`, optional | The itinerary, for a flight. |
| `linkSent` | `boolean` | `true` when the shopper holds this product's page. |

`productAction(merchant)` returns `reserve` for `reservations`, `book` for `flights`, `hotels`, `tickets` and `classes`, and `buy` for every other merchant.

## BuyToolStore

| Field | Type | Description |
| - | - | - |
| `id` | `string` | Agentcard's id for the store, which the model passes as `store_id`. |
| `name` | `string` | The store's name. |
| `url` | `string` or `null` | The store's page at the merchant. |
| `etaMinutes` | `number` or `null` | The delivery estimate in minutes. |
| `imageUrl` | `string` or `null` | The picture the store's card shows at the merchant. |
| `linkSent` | `boolean` | `true` when the shopper holds this store's page. |

## What the model reads

`buy.forModel(result)` returns `BuyToolModelView`, the answer with every link taken out. An address with a path or a scheme is removed, and a bare one, such as `walmart.com`, reads as the site's name.

| Field | Type | Description |
| - | - | - |
| `status` | `BuyToolStatus` | See [Status](#status). |
| `next_action` | `BuyNextAction` or `null` | The one thing to ask the shopper for in this message. |
| `cart_building` | `{ store: string }`, optional | Set while the store's checkout builds the cart. |
| `reply` | `string` | Agentcard's answer, with every link taken out. |
| `cart` | `BuyToolModelCart` or `null` | The newest cart. |
| `carts` | `BuyToolModelCart[]` | Every cart on the table, the newest last. |
| `products` | `BuyToolModelProduct[]` | Each with `product_id`, `name`, `store`, `price` and `link_sent`. |
| `approval_expires_at` | `string` or `null` | When the approval link stops working. |
| `link_expires_in` | `string` or `null` | How long the newest link still works, in words such as `about 24 hours`. |
| `shopper_need_not_reply` | `boolean` | `true` when the shopper need not reply. |
| `shopper_already_told_looking` | `boolean` | `true` when the shopper already knows the request is being worked on. |
| `link_sent` | `boolean` | `true` when the shopper already has the link this answer needed. |
| `links_sent` | `Array<'vault' \| 'approval' \| 'store_login'>` | Which links went out with this answer. |
| `product_links_sent` | `number` | How many products the shopper holds as cards. |
| `pick` | `{ product_id, in_cart }` or `null` | The product this call picked. |
| `stores` | `BuyToolModelStore[]` | Each with `store_id`, `name`, `eta_minutes` and `link_sent`. |
| `store_links_sent` | `number` | How many store pages the shopper holds. |
| `store_pick` | `{ store_id, selected }` or `null` | The store this call picked. |

`BuyToolModelCart` has `store`, `items`, `fees`, `fee_lines`, `tip`, `total`, `total_is_estimate`, `total_leaves_out` and `max_charge`. Each item has `product_id`, `name`, `qty`, `price`, `line_total`, `options` and `variant`. Prices are text such as `\$13.31`.

## Standalone helpers

The package root exports the functions the tool uses, for a loop that reads results itself.

| Export | Description |
| - | - |
| `BUY_TOOL_NAME` | `buy`. |
| `BUY_TOOL_DESCRIPTION` | The rules the model follows, as one string. |
| `buyToolDefinition()` | A fresh copy of the tool's name, description and input schema. |
| `parseBuyToolInput(value)` | Checks the model's input and returns `BuyToolInput`. Throws a `TypeError` the model can act on. |
| `forModel(result)` | The model's view of a `BuyToolResult`. |
| `forModelError(error)` | The error's message with every link taken out, and the original on `cause`. A `TypeError` stays a `TypeError`. Hand errors from `buy.answer` to it before a model reads them. |
| `replyAsWritten(result)` | Agentcard's reply when the answer only asks the shopper something, else `null`. |
| `stripLinks(text)` | The text with every link taken out. |
| `instantAck(text, options)` | The instant reply for a message, or `null`. Takes `previous`, `nextAction`, `asked` and `stores` as options. |
| `instantReaction(text, options)` | `👀`, or `null`, decided as `instantAck` decides. |
| `ACK_REACTION` | `👀`. |
| `keepsYes(text)` | `true` when the shopper's words leave a yes they already gave standing, such as `ok` or `done`. |
| `ackFor` | The earlier name of `instantAck`. |

The `instantAck` options work as follows: `previous` is the reply you sent last, so the next one comes from another template. `nextAction` is the last answer's `next_action`. `asked` is the last message the shopper read. `stores` holds the names of the stores the last answer listed.

```ts ack.ts theme={null}
import { instantAck, instantReaction } from '@agent-cards/commerce-sdk';

instantAck('can you find me usb-c cables under $20'); // 'on it, looking for usb-c cables under $20', or another template
instantReaction('yes'); // null
```

The first call returns a reply that names the request, and the second returns `null` because `yes` starts no search.


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