Skip to main content
This guide takes the iMessage agent from Create an iMessage agent and connect the Vault and teaches it to buy things. The user texts what they want. The agent finds it at Amazon, Walmart, Target, Best Buy, DoorDash, or another store through Agentcard’s Commerce SDK, shows the cart, and places the order when the user says yes, paying with the card they saved in the Vault. The first guide covered the plumbing: Linq, Redis, and your Agentcard credentials. This guide covers the code that makes the agent shop. What you end up with: a phone number where a user texts “two packs of AA batteries from Amazon”, gets a photo, a name, and a price back within a minute, says which one they want, taps an approval link, and reads “order placed” in the same thread, with nothing else to type.

Before you start

  • An iMessage agent that receives a text and replies in the same thread. The first guide builds one on Linq, Vercel, and Redis.
  • Agentcard sandbox credentials, from Commerce SDK, Start implementing in the dashboard. Quickstart shows where.
  • Node 18 or later.
The sandbox stops before payment. A confirm there ends declined, with no approval link and no order. Checking the approval and the placed order needs live credentials, as Go live describes.

How it works

The user’s phone number is their externalUserId. Agentcard connects them to your company the first time they ask, with one Vault link, and never needs a code or an account.

1. Install the SDK

Build one AgentcardClient and keep one buy tool for each phone number, as Quickstart shows. Set ack: 'text' on each tool, because iMessage shows no reactions to the agent:
agentcard.ts
Linq sends a message from your number with a list of parts. A text part holds words, and a media part holds the web address of an image, which Linq fetches and delivers as a photo.
linq.ts
iMessage shows a URL that stands alone in a message as a preview card, and it mangles a URL with words attached. So every link goes out as its own message, never inside a sentence. For a Vault or approval link, anything glued to the URL turns it into a broken link that opens the Vault’s sign-in page.

3. Answer each text in its own turn

Open a turn for each text and hand the app the turn in order with onItem. The turn sends the instant reply, then the product cards, then your model’s words about them, and it holds each step link until the message that asks for it went out. Answering each shopper message in its own turn explains each option.
chat.ts
loadHistory and saveHistory stand for your own code. Save the history after every turn, because the model reads from it that the user saved a card or got an order. Use the user’s phone number in the same form every time, such as +14155550100, so the same person always maps to the same tool.

4. Hear from Agentcard at a webhook endpoint

A Vercel function ends each request after a few seconds, so take Agentcard’s events at a webhook endpoint instead of listen. Mount agentcardWebhook on the route where the first guide points Agentcard, and run the next step for the user when a message advances the purchase. Taking the next step once the shopper finishes outside the chat explains the turn.
app/agentcard/webhooks/route.ts
runNextStep is onText for a turn that nextStep opened: it runs the model on step and sends the answer in place of message.text. saveAssistantMessage appends an assistant message to the user’s history. A Vercel function keeps a user’s tool only while its own instance lives, and the chat route and this route can run in different instances. The next step reads where the purchase stands from Agentcard, so the card link, the cart, and the ask for the yes still arrive. A yes the user already gave in the chat, and the first ask of a user who was not connected yet, stay in the instance that took them, so the next step asks for the yes again, or asks the user to carry on from what they last asked for. To take both with nothing typed, run the chat and agentcard.listen together in one long-running process. Create the webhook endpoint, subscribed by name to the three events that carry a message for a user. A catch-all * alone does not turn purchase.message on. Exchange your client ID and secret for an organization token first, as step 6 of the first guide shows.
The response carries the endpoint’s signing secret once. Save it as AGENTCARD_WEBHOOK_SECRET on Vercel and deploy again. The handler answers 400 to a delivery whose signature does not match, so until the secret is set, no message arrives.

5. Text it

From the phone you activated the Linq sandbox with, text the Linq number:
A new user gets a Vault link first, alone in its own message. Open it, connect with the code 111111, and save any of Stripe’s published test cards. The sandbox sends no card-saved message, so text the agent again, and it texts the products for your ask. Send “the second one”, and the agent adds that product and texts the cart. Send “yes”, and the confirm ends declined, because the sandbox stops before payment.

Go live

Going live makes the approval link, the passkey tap, and the placed order real. The code does not change.
  1. Switch to Live credentials and choose how to pay the fee, as Quickstart describes.
  2. Replace AGENTCARD_CLIENT_ID and AGENTCARD_CLIENT_SECRET on Vercel with the production values.
  3. Create a live webhook endpoint with the live credentials, because an endpoint belongs to a mode, and replace AGENTCARD_WEBHOOK_SECRET with its secret.
  4. Deploy, then buy one inexpensive item with your own card. Check that the approval link arrives alone, and that “order placed” arrives after the tap with nothing typed.

If nothing comes back

  • The Vault link opens a sign-in page instead of the card form. Something was glued onto the URL. Send every link as its own message.
  • The agent asks “did you save your card?” after the user did. The model never read the card message. Check that saveAssistantMessage runs, and that loadHistory returns what saveHistory stored.
  • The card is saved and nothing happens. Check the endpoint’s deliveries under Settings, Developers, Webhooks in the dashboard. A 400 means AGENTCARD_WEBHOOK_SECRET does not match the endpoint. A 500 means onMessage threw, and Agentcard delivers the event again.
  • Messages arrive in the wrong order. Hand the turn to your app with onItem and write the model’s text through turn.messageStream().
  • A product has no photo. The store sent none, so imageUrl is null. Send the name and price alone.

Where to go next