A2A endpoints
Three new REST endpoints that let buyer agents discover, negotiate, and settle with your shop — without ever opening a browser.
Cartwright v0.2.0 ships three Agent-to-Agent endpoints that turn your shop into a Headless Merchant. A buyer agent can read your published Agent Card, negotiate price via a deterministic engine, and release funds against a Proof-of-Task-Execution — all over plain HTTPS, no GUI involved.
All three live in app/api/ and are gated behind brand.features.a2a (default false). A shop scaffolded with --template agent-marketplace has the flag on; everything else returns 404 to keep your storefront surface clean.
GET /api/agent-card
Discovery endpoint. A buyer agent calls this first to learn what your shop sells, which payment rails you accept, and what negotiation policy you publish.
curl https://example-shop.app/api/agent-cardResponse is a signed SignedAgentCard:
{
"payload": {
"version": 1,
"shopId": "example-shop.app",
"shopName": "Example",
"issuedAt": "2026-05-25T08:00:00.000Z",
"expiresAt": null,
"capabilities": [
{ "id": "catalogue-feed", "name": "Catalogue feed (per 1000 records)",
"anchorPriceMinor": 5900, "floorPriceMinor": 4000, "currency": "DKK" }
],
"paymentRails": ["stripe"],
"negotiationPolicy": { "concessionRate": 0.5, "maxRounds": 5 }
},
"signature": "base64-ed25519-signature",
"publicKey": "base64-ed25519-public-key",
"_meta": { "version": 1, "signedAt": "...", "expiresAt": null }
}The buyer agent verifies the signature offline against the embedded public key before trusting any field. Signed with ed25519 (lib/a2a/agent-card.ts).
If no AgentCard row exists yet, the endpoint returns 503 no_agent_card_configured — buyer agents MUST refuse to negotiate with an un-carded shop.
POST /api/negotiate
Negotiation endpoint. Wraps the deterministic Anchor-and-Resume engine — never an LLM. The handler authenticates via Bearer token, runs the Guardian middleware, then invokes the engine.
curl -X POST https://example-shop.app/api/negotiate \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"agentId": "buyer-001",
"jti": "unique-jwt-id-001",
"signedJwt": "eyJ...",
"scopes": ["negotiate"],
"floorMinor": 4000,
"anchorMinor": 5900,
"concessionRate": 0.5,
"currentOffer": { "priceMinor": 5900, "quantity": 1, "validUntil": "2026-05-26T08:00:00Z" },
"counterOffer": { "priceMinor": 3000, "quantity": 1, "validUntil": "2026-05-26T08:00:00Z" },
"round": 2,
"maxRounds": 5
}'Response:
{
"decision": "counter",
"nextOffer": { "priceMinor": 4950, "quantity": 1, "validUntil": "..." },
"reasoningCodes": ["COUNTER_BELOW_FLOOR", "CONCESSION_APPLIED"]
}decision is accept | counter | hold | reject. reasoningCodes is a closed enum (9 values) that explains why — the LLM translation layer can render them as natural-language buyer comms downstream, but the engine itself never speaks prose.
The engine has NO LLM imports. This is a hard rule per the Master Plan §3.2 — any prompt-injection that moves money is automatically blocked by the P2K scanner. If you build atop this, never call chatModel() from a route that touches money.
POST /api/escrow/verify
Fund-release endpoint. Buyer agent submits a Proof-of-Task-Execution (PoTE) to release escrowed funds.
curl -X POST https://example-shop.app/api/escrow/verify \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"agentId": "buyer-001",
"jti": "unique-jwt-id-002",
"signedJwt": "eyJ...",
"scopes": ["escrow.release"],
"escrowTxId": "ctx_abc123",
"proofType": "hash",
"proofPayload": {},
"submittedHash": "abc123..."
}'proofType discriminates four verifier paths:
proofType | What's verified |
|---|---|
hash | Constant-time comparison against EscrowTransaction.expectedHash. Pass → state machine transitions funded → released. |
delivery | Carrier-side delivery confirmation (signed webhook). For Phase 8, recorded as pending awaiting external confirmation. |
signature | Buyer's ed25519 signature over the artifact hash. Phase 8 records as pending; admin approves manually in /admin/agentic. |
webhook | External system (Stripe, shipping) webhook event id. Records pending until external confirmation. |
On pass, the escrow row transitions to released atomically (state-machine-guarded — see escrow state machine). On fail, the row stays unchanged so a human can dispute via /admin/agentic.
Discovery surface
The Agent Card endpoint is the discovery point, but Cartwright also publishes:
/.well-known/mcp.json— Model Context Protocol discovery (Cartwright v1)/.well-known/ucp— Universal Commerce Protocol/llms.txt— LLM crawler hint/api/v1/tools— typed catalogue of every action an agent can take
A buyer agent has multiple ways to find your shop — gh api-style discovery, MCP probe, or just curl-ing well-known paths.
Authentication
All three POST endpoints require a Bearer token (Authorization: Bearer <api-key>). API keys are managed via /admin/api-keys and scoped (e.g. ["negotiate", "escrow.release"]). The Guardian middleware additionally enforces the shop's agenticPolicyJson legislation rules before any handler runs.
Enabling on your shop
// brand.config.ts
features: {
a2a: true, // turns the 3 endpoints on
acp: true, // enables /api/acp/* (commerce protocol)
webshop: false, // optional: hide buyer-facing GUI for pure-A2A shops
adminAgenticDashboard: true,
}Or scaffold with npx create-cartwright my-agent-shop --template agent-marketplace — the template ships these flags on by default.
WebMCP (in-browser agent tools)
Expose storefront actions — search, cart, navigate — as browser-native tools to in-browser AI agents via document.modelContext, so they act reliably instead of scraping the DOM.
Guardian middleware
The third independent branch of Separation-of-Power — every agentic call passes through Guardian before any money or escrow state moves.