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

# Taking the next step once the shopper finishes outside the chat

> Move the purchase on the moment a shopper saves a card, signs in to a store, or declines an approval, without waiting for them to say ok.

Some steps happen outside the chat. The shopper saves a card from the Vault link, signs in to DoorDash from the sign-in link, or declines the approval. Agentcard tells your server the moment each one ends.

If your server posts Agentcard's message as it is, the shopper reads "Your Visa ending 4832 is saved, so you're ready to check out." and then has to say "ok" before anything moves. Take the next step for them instead, and the shopper reads one message that does the next thing: the approval link, the add-card link, or the total and the ask for their yes.

## Open a turn for the message

A message whose `advances` is true ends a step the shopper took outside the chat. `NEXT_STEP_KINDS` lists the kinds that advance: `card_added`, `store_connected`, and `approval_declined`. Call `buy.nextStep(message)` in your `onMessage`. It returns a turn to run your model on, or null when there is no step to take.

```ts server.ts theme={null}
import type { BuyToolTurn } from '@agent-cards/commerce-sdk';
import { agentcard, buyFor } from './agentcard';

// Each shopper's model runs go in order, so the chat's own turns wait on them too.
const runs = new Map<string, Promise<void>>();

agentcard.listen({
  onMessage: (shopperId, message) => {
    const step = buyFor(shopperId).nextStep(message);
    if (!step) return postToChat(shopperId, message.text); // nothing to take: post the text as it is

    // Run the model without awaiting, so the listener moves on to the next event.
    // One shopper's runs go in order, so an older run never writes its history over a newer one.
    const run = (runs.get(shopperId) ?? Promise.resolve())
      .then(() => runModel(shopperId, step))
      .catch(async () => {
        // The turn failed: release it so its text reaches later turns, and post the text instead.
        step.release();
        await postToChat(shopperId, message.text);
        agentcard.delivered(shopperId, message); // only once the post went out
      })
      // The fallback post failed as well, so nothing is recorded and a redelivery opens the turn again.
      .catch(() => {});
    runs.set(shopperId, run);
  },
});

async function runModel(shopperId: string, turn: BuyToolTurn) {
  const runner = anthropic.beta.messages.toolRunner({
    model: 'claude-opus-5-5',
    max_tokens: 16000,
    tools: [turn],
    messages: turn.messages(histories.get(shopperId)),
  });
  const answer = await runner;
  histories.set(shopperId, runner.params.messages);
  await postToChat(shopperId, textOf(answer), turn.links);
}
```

`anthropic`, `histories`, `postToChat`, and `textOf` stand for your own code. The turn is a `buy.turn` in every way but one: no shopper wrote anything, so `turn.messages(history)` ends with the event, marked as written by the system, and the instruction to take the next step now in one short message. The model never asks "you're all set, what's next?".

Post the model's answer in place of `message.text`. The shopper never reads `message.text`, and the client never hands that text to a later turn as words the shopper read.

Wait for each post. A post of the model's answer that fails sends the turn to the fallback, and a fallback post that fails records nothing as delivered, so Agentcard's redelivery of the event opens the turn again.

## Know what the shopper reads next

| The shopper finished | The next message |
| - | - |
| Saving a card, after their yes stopped for it | The approval link: `i sent you a link to approve the payment for your chicken salad. it works for about 15 minutes, and the most you'll be charged is $19.02.` |
| Saving a card, with no yes yet | The total and the ask for the yes: `your Souvla chicken salad comes to $17.43 with fees. should i go ahead and place the order?` |
| Signing in to the store, with a card still to add | The add-card link: `next, add a card with the link i sent. it works for about 24 hours.` |
| Signing in to the store, with a card on file | The total and the ask for the yes, or the approval link after a yes that stopped for the sign-in. |
| Declining the approval | `you declined the payment, so you haven't been charged. your chicken salad is still in the cart, so just say yes if you want a new approval link, or tell me what to change` |

## Keep the shopper's decision theirs

The turn moves the purchase only up to the shopper's own decision, and the tool holds it there whatever the model calls.

* No call of the turn sends an ask to Agentcard, because Agentcard could read the model's words as the shopper's go-ahead. Each call reads where the purchase stands and answers with the next step: the card link the sign-in held back, or the cart and the ask for the shopper's yes.
* A confirm goes in only for the yes the shopper already gave in the chat, such as "lets place it", when that confirm stopped for the card or the sign-in they finished. It goes in once, for that cart's hash, so Agentcard refuses it if the cart changed. A shopper message after the yes that is not a plain go-ahead, such as "wait, make it two", takes the yes back.
* Any other confirm the model sends is refused by the tool, and nothing reaches Agentcard.
* The approval in the Vault is still the shopper's. The confirm sends the approval link, and Agentcard places the order only once the shopper approves it.
* A shopper who was not connected yet when they asked gets their own first ask run, in their words, once their card is saved.

## Handle the cases with no step

`buy.nextStep` returns null in four cases, and your code posts `message.text` then:

* The message's `advances` is false, such as `order_placed`, whose `text` is already the whole message.
* The message is for another shopper, or its link closed.
* The tool already took a turn for the message.
* The message arrives while a call for the shopper's own message is still running. That call's answer tells the model where the purchase stands.

When your agent cannot finish the turn, call `step.release()` before you post `message.text`. The text then reaches later turns once `agentcard.delivered` records it, and a redelivery of the event opens a turn again.

### Run the listener and the model in one process

The client keeps each message in memory, at most 20 for each shopper, so `listen` or the webhook handler and the turn run in one process on one client. When your model runs elsewhere, such as a serverless chat route, keep each message where your chat history lives, as an assistant message, and the model reads it from the history. The Quickstart covers the webhook handler for serverless hosts.

## Check it in the sandbox

1. Send "two packs of AA batteries from Amazon" as a test shopper nobody connected, and open the Vault link.
2. Save a card with the code `111111`. Do not type anything in the chat.
3. Check that the chat posts one message that does the next thing, such as the ask for your yes or the approval link, with nothing typed after the card saved.
4. Check that the agent ran your first ask, "two packs of AA batteries from Amazon", in your words once your card was saved, without you asking again.
5. Make your model throw during `runModel`. Check that the shopper still reads `message.text`.


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