How an AI sale actually happens
End-to-end walkthrough — a buyer agent discovers your shop, negotiates and checks out, then where escrow, proof and audit stand in today's engine.
This guide follows a single transaction from "buyer agent has never heard of your shop" to "funds are in your Stripe account". Every step shows the actual HTTP call and the actual response. The point is to make the abstract concrete: when we say "headless merchant", this is the conversation that proves the claim.
The walkthrough assumes a Cartwright shop scaffolded with --template agent-marketplace (so brand.features.a2a and brand.features.acp are both true), with brand.acp.enabled and ecommerceEnabled then set to true by hand. The template leaves both false, and without them the feed (Step 3) and the ACP checkout (Step 5) answer 404. A pure-A2A shop, a pure-ACP shop, or a hybrid all draw from the same surface — they just answer 404 on the endpoints they opted out of.
Cast of characters
- Your shop:
example-shop.app— a Cartwright deploy - The buyer agent: a process somewhere on the internet acting for an end user (or for another business)
- Stripe: the payment processor your shop authorised at setup
- The audit ledger: the
AgenticJWTrows the Guardian writes on the A2A calls
Step 1 — Discovery
The agent has a buyer intent ("find 100 wholesale sunglass frames in mahogany under DKK 65 per unit") and a list of candidate shops. It probes Cartwright's discovery surface to learn what example-shop.app is.
# Cartwright publishes four discovery hints
curl https://example-shop.app/llms.txt
curl https://example-shop.app/api/v1/tools
curl https://example-shop.app/.well-known/mcp.jsonllms.txt is the LLM-crawler hint (free-form). /api/v1/tools is the typed catalogue of available actions. /.well-known/mcp.json is the MCP discovery card. Together they tell the agent: "this shop is reachable, here are the entry points, here are the auth requirements".
Step 2 — Agent Card fetch
The agent now wants the cryptographically-signed truth: who owns this shop, what they sell, what rails they accept, what their negotiation policy is.
curl https://example-shop.app/api/agent-cardResponse (truncated):
{
"payload": {
"version": 1,
"shopId": "example-shop.app",
"shopName": "Atelier Sunglass Co.",
"issuedAt": "2026-05-25T08:00:00.000Z",
"expiresAt": null,
"capabilities": [
{ "id": "sunglass-frames-wholesale",
"name": "Sunglass frames, wholesale lots of 50–500 units",
"anchorPriceMinor": 6500,
"floorPriceMinor": 4200,
"currency": "DKK" }
],
"paymentRails": ["stripe", "escrow-poTE"],
"negotiationPolicy": { "concessionRate": 0.5, "maxRounds": 5 }
},
"signature": "...",
"publicKey": "..."
}The agent verifies the ed25519 signature offline against the embedded public key. If it doesn't verify, the agent walks away — no negotiation with un-carded shops. See A2A endpoints for the signing setup.
Step 3 — Product feed
The Agent Card describes capabilities; the feed gives the actual SKUs. The agent pulls the JSONL product feed.
curl https://example-shop.app/api/acp/feed{"item_id":"mahogany","title":"Mahogany frame, classic round","url":"https://example-shop.app/en/product/mahogany","price":"65.00 DKK","availability":"in_stock","brand":"Atelier"}
{"item_id":"walnut","title":"Walnut frame, wayfarer cut","url":"https://example-shop.app/en/product/walnut","price":"58.00 DKK","availability":"in_stock","brand":"Atelier"}The agent matches the buyer intent against the feed (mahogany, 100 units, under DKK 65). mahogany at DKK 65 list price is at the ceiling; the agent decides to negotiate.
Step 4 — Negotiation
The agent obtains an API key from the shop out of band: the admin issues one at /admin/api-keys. /api/negotiate checks that the key is valid, not which scopes it carries; what the agent may do is the shop's agentic policy, which the Guardian checks. It then opens a negotiation.
curl -X POST https://example-shop.app/api/negotiate \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"agentId": "buyer-northbound",
"jti": "neg-7f3e9a1b-2026-05-25-001",
"signedJwt": "eyJ...",
"scopes": ["negotiate"],
"floorMinor": 4200,
"anchorMinor": 6500,
"concessionRate": 0.5,
"currentOffer": { "priceMinor": 6500, "quantity": 100, "validUntil": "2026-05-26T08:00:00Z" },
"counterOffer": { "priceMinor": 5000, "quantity": 100, "validUntil": "2026-05-26T08:00:00Z" },
"round": 1,
"maxRounds": 5
}'{
"decision": "counter",
"nextOffer": { "priceMinor": 5750, "quantity": 100, "validUntil": "2026-05-26T08:00:00Z" },
"reasoningCodes": ["COUNTER_BELOW_FLOOR", "CONCESSION_APPLIED"]
}The Anchor-Resume engine is pure TypeScript — no LLM. It received an offer below the floor (DKK 50), concedes by half the gap between anchor (DKK 65) and the previous offer (DKK 65 → 57.50), returns DKK 57.50, and explains why with reasoning codes.
Two more rounds happen; the agent settles at DKK 56 per unit, total DKK 5600 for 100 units.
Step 5 — Checkout
The agent now creates an ACP checkout session. The ACP routes take no API key, so this call and complete send none. Two limits of today's engine shape this step: a session prices every line at the catalog price, with no field for the price Step 4 agreed, and one line takes 1 to 10 units. So the example orders 10 frames at list price. Each line item names its item with item_id, the id the feed listed in Step 3.
curl -X POST https://example-shop.app/api/acp/v1/checkout_sessions \
-H "Content-Type: application/json" \
-d '{
"line_items": [
{ "item_id": "mahogany", "quantity": 10 }
],
"buyer": { "email": "orders@northbound.example" },
"fulfillment_address": {
"name": "Northbound Buyer Agent / Atelier B2B",
"address_line1": "Esplanaden 12",
"postal_code": "1263",
"city": "Copenhagen",
"country": "DK"
}
}'Response:
{
"id": "acs_9b2d4f6a8c0e4a1b9d3f5a7c9e1b3d5f",
"status": "ready_for_payment",
"currency": "dkk",
"line_items": [
{
"id": "mahogany",
"product_id": "cm9x2k7q40002efgh",
"variant_id": null,
"slug": "mahogany",
"sku": null,
"name": "Mahogany frame, classic round",
"quantity": 10,
"unit_amount": 6500,
"amount_total": 65000
}
],
"fulfillment_address": { "name": "Northbound Buyer Agent / Atelier B2B", "address_line1": "Esplanaden 12", "postal_code": "1263", "city": "Copenhagen", "country": "DK" },
"fulfillment_option": null,
"buyer": { "email": "orders@northbound.example", "name": null, "phone": null },
"totals": { "subtotal": 65000, "total_shipping": 0, "total_discount": 0, "amount_total": 65000 },
"messages": []
}Pricing was computed server-side by lib/pricing.ts from the catalog price, in minor units: the subtotal, the shop's flat shipping rate (waived from its free-shipping threshold, DKK 499 by default, so 0 here) and any discount_code. There is no separate tax line. product_id is the shop's internal database id; it appears in responses only and is not an input field.
curl -X POST https://example-shop.app/api/acp/v1/checkout_sessions/acs_9b2d4f6a8c0e4a1b9d3f5a7c9e1b3d5f/complete \
-H "Content-Type: application/json" \
-d '{ "shared_payment_token": "spt_…", "idempotency_key": "ckt-7f3e9a1b-2026-05-25-001" }'complete charges the Stripe Shared Payment Token and creates the Order from the session's line items. It confirms the PaymentIntent on the spot (confirm: true, off_session: true, automatic capture) and expects succeeded, so the money has moved once it returns, with no hold left for a proof to release. It answers 501 until the shop sets ACP_PAYMENT_COMPLETION=1; see ACP.
Step 6 — Escrow, as the engine stands
No engine code opens or funds an escrow row. The EscrowTransaction table exists (prisma/schema.prisma), but nothing creates a row: not the ACP session, not complete, and not the Stripe webhook at /api/webhook/stripe, which handles payment events for orders and never touches the table. There is no GET for an escrow; POST /api/escrow/verify is the only escrow route. Step 7 therefore applies only to a row your own code writes, with status funded.
Step 7 — Proof submission
The agent submits a Proof-of-Task-Execution against that row. A delivery proof carries the carrier and its trackingNumber.
curl -X POST https://example-shop.app/api/escrow/verify \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"agentId": "buyer-northbound",
"jti": "rel-7f3e9a1b-2026-05-25-001",
"signedJwt": "eyJ...",
"scopes": ["escrow.release"],
"escrowTxId": "cm9x2k7q40003ijkl",
"proofType": "delivery",
"proofPayload": {
"carrier": "GLS",
"trackingNumber": "JJD000123456789"
}
}'The handler (app/api/escrow/verify/route.ts):
- Answers 404 unless
brand.features.a2ais on, and 422 on a body its schema refuses. - Authenticates the Bearer token (401 without a valid key). The key's scopes are not checked.
- Loads the escrow row (404
escrow_not_foundwithout one). - Runs the Guardian middleware: the shop's agentic policy must allow
escrow.releasefor this agent, thejtimust be new, the amount must be within the policy's cap and the payment rail on its allowlist (403 otherwise). It writes oneAgenticJWTrow either way. Thescopesin the body are recorded in that row, not checked. - Refuses a row that is not
fundedordisputed(409). - Runs the verifier and writes a
PoTEProofrow, whatever the result. Only apassmoves the row toreleased.
Today no proof type passes. delivery, signature and webhook proofs are recorded as pending, and a hash proof fails, because the route has no expected hash to compare with. So the row stays funded:
{
"escrowStatus": "funded",
"proofId": "cm9x2k7q40004mnop",
"verifierResult": "pending",
"verifierMessage": "Delivery proof recorded; awaiting carrier webhook confirmation."
}Step 8 — Funds disbursement
There is nothing for escrow to disburse: Step 5's complete already captured the charge, and the funds follow your Stripe payout schedule from there. A proof that fails or pends leaves the escrow row as it was; it does not move it to disputed. /admin/agentic is read-only: it counts escrow rows by status and lists the disputed ones, and force-release and force-refund are not built yet.
Step 9 — Audit
The rows this walkthrough writes are AgenticJWT rows, one per Guardian call, pass or fail, with the request path, the jti and the scopes the agent sent. The Agent Card fetch, the feed and the ACP routes write none, in AgenticJWT or AuditLog. With adminAgenticDashboard on, /admin/agentic lists the last 24 hours of them, up to 50.
| Step | Request | AgenticJWT row |
|---|---|---|
| 2 | GET /api/agent-card | none |
| 3 | GET /api/acp/feed | none |
| 4 | POST /api/negotiate, rounds 1 to 3 | one per round, pass |
| 5 | ACP create and complete | none |
| 7 | POST /api/escrow/verify | one, pass (the Guardian's verdict; the proof's own result is in the PoTEProof row) |
What didn't happen
Read what's not in the walkthrough — that's where the security work is:
- No LLM was asked to decide the price. The Anchor-Resume engine is pure TS; the P2K scanner gates every commit that would try.
- No negotiation round and no proof skipped the Guardian:
/api/negotiateand/api/escrow/verifyrun it before they act, and they are the only routes that do. The Agent Card fetch (GET /api/agent-card) is A2A-gated but runs no Guardian: whoever reaches it gets the card, whatever the policy'sagent-card.readscope or blocked-agent list says. The ACP routes,completeincluded, run none either; their gates arebrand.acp.enabled, the schemas andACP_PAYMENT_COMPLETION. - No escrow was released on an unverified proof, or on any proof yet: only a
passmoves a row toreleased, and today's verifier passes none (Step 7). - No agent negotiated or submitted a proof outside the shop's agentic policy: the Guardian checks
negotiateandescrow.releaseagainst it. With no policy set it allows nothing, and no admin screen or tool sets one yet, so thepassrows in Step 9 need a policy written straight toBrandingSettings.agenticPolicyJson. An API key's own scopes are not checked on these routes, and the ACP routes take no key.
The walkthrough is a happy path. The Guardian + escrow + audit machinery is most of the code; it just happens to stay quiet when the happy path is followed.