# AGENTS.md — LLM Integration Guide for Pizza x402

> This document is specifically designed for LLM agents (ChatGPT, Claude, Gemini, etc.) to integrate with the Pizza x402 API. It provides structured tool definitions, example flows, and deterministic response schemas.

## Overview

Pizza x402 is a **conversation-first pizza ordering API**. The flow is:

```
GET /v1/menu → POST /v1/orders/quote → POST /v1/orders/confirm → payment → POST /v1/orders/confirm → GET /v1/orders/{id}
```

**Base URL:** `http://localhost:3000` (dev) or the current live Cloud Run host `https://api.pizzax402.com`

> **Agent Warning:** Launch payment strategy: Stripe-only. Do not prioritize Coinbase x402, USDC, Base, wallet setup, or crypto checkout for launch unless the founder explicitly reopens that decision. x402 remains a long-term architectural direction, not a launch blocker.

**Payment:** Launch real payments use Stripe only. Merchants must complete Stripe Connect Accounts v2 onboarding and have required payment capabilities ready before Pizza x402 should expose a live storefront or card checkout. Coinbase x402 / USDC / Base remains future-facing and must be treated as experimental unless the founder explicitly reopens the decision. Always read `paymentRails` from merchant bootstrap and `availablePaymentMethods` from the quote, but launch agents should prefer `paymentMethod: "card"` and must not require customer wallet setup.

**Rate limiting:** Public `/v1/*` endpoints may return `429 RATE_LIMITED` when burst budgets are exhausted. Respect the `Retry-After` header and the JSON `retryAfterMs` field before retrying.

**Optional public auth:** Deployments can enable `PUBLIC_API_AUTH_MODE=api-key` to require scoped `X-API-Key` headers on `/v1/*` routes. Local/demo mode stays open by default.

## Merchant Bootstrap and Request-Bound Context

Shared storefronts should bootstrap merchant context from:

- `GET /v1/merchants/{merchantHandle}/info`
- `GET /v1/merchants/{merchantHandle}/legal`
- `GET /v1/merchants/{merchantHandle}/contact-details`

Until customer-facing storefront URL topology is finalized, shared ordering calls may propagate merchant context with `X-Merchant-Handle: <merchantHandle>` on:

- `GET /v1/menu`
- `GET /v1/wallet-setup`
- `POST /v1/orders/quote`
- `POST /v1/orders/confirm`
- `GET /v1/orders/{id}`

If public auth is enabled, merchant bootstrap endpoints require the scoped public permission used for merchant metadata (`merchant:read`).

## Locale Contract

Locale fields are split by surface:

- `PlatformLocale` is the merchant dashboard/system UI locale. It is limited to `en` and `fr`, including `/merchant/session/preferences` `uiLocale`.
- `ContentLocale` is merchant-authored storefront content. It accepts valid BCP 47 tags such as `en`, `fr`, or `sv-SE`.
- Use stable locale-specific storefront URLs (`/{merchantHandle}/{contentLocale}`) and pass the route locale to public API calls as `?locale=<contentLocale>`.
- Keep the unprefixed merchant URL (`/{merchantHandle}`) as the merchant-default / `x-default` URL. Do not pass `?locale` there.
- Public API `locale` query parameters plus `requestedLocale`, `resolvedLocale`, `availableLocales`, and `suggestedLocale` are `ContentLocale`.
- `defaultContentLocale`, `publishedStorefrontLocales`, `sourceLocale`, and `translations` map keys are `ContentLocale`; at most 8 storefront locales can be published per merchant.
- `Accept-Language` is a hint for `suggestedLocale` only; it does not change the content served for an unprefixed storefront URL.
- Platform/system copy falls back to English. Merchant-authored content falls back to its declared source locale.

## Quickstart Branch Guide

For deterministic local onboarding, use:

- `docs/public/quickstarts/first-paid-order-5-minutes.md`

This quickstart is launch-oriented: use the Stripe card branch for real orders. Crypto/x402 examples are future/experimental and should not be used as launch proof.

Expected checkpoints:

- Card: `quoteId` -> `checkoutSessionId` -> Stripe Checkout success -> `orderId` -> terminal `status=paid`
- Crypto: `quoteId` -> `paymentRequestId` (`402`) -> `orderId` -> terminal `status=paid`

You can also run the deterministic example client directly:

```bash
node examples/node-agent/index.ts --paymentMethod card
```

---

## Tool Definitions for LLM Agents

### Merchant Bootstrap Helpers

Use these endpoints before menu/quote/confirm if the storefront needs merchant identity, hours, payment rails, legal pages, or contact details.

**Merchant info**
```http
GET /v1/merchants/{merchantHandle}/info
```

Representative response:
```json
{
  "merchantHandle": "stockholm-sourdough",
  "defaultContentLocale": "sv-SE",
  "publishedStorefrontLocales": ["sv-SE", "en", "fr"],
  "requestedLocale": "",
  "resolvedLocale": "sv-SE",
  "availableLocales": ["sv-SE", "en", "fr"],
  "suggestedLocale": "en",
  "name": "Stockholm Sourdough Pizza",
  "address": "Stora Nygatan 1, 111 27 Stockholm, Sweden",
  "city": "Stockholm",
  "country": "SE",
  "requestContext": {
    "transport": "header",
    "header": "X-Merchant-Handle",
    "value": "stockholm-sourdough"
  },
  "fulfillment": { "type": "pickup" },
  "hours": {
    "timezone": "Europe/Stockholm",
    "windows": [
      { "day": "monday", "opensAt": "17:00", "closesAt": "22:00" }
    ],
    "notes": ["Pickup only."]
  },
  "paymentRails": {
    "defaultCheckoutMethod": "card",
    "available": ["card"]
  },
  "menuUrl": "https://api.pizzax402.com/v1/menu",
  "legalUrl": "https://api.pizzax402.com/v1/merchants/stockholm-sourdough/legal",
  "contactDetailsUrl": "https://api.pizzax402.com/v1/merchants/stockholm-sourdough/contact-details",
  "walletSetupUrl": "https://api.pizzax402.com/v1/wallet-setup"
}
```

**Merchant legal pages**
```http
GET /v1/merchants/{merchantHandle}/legal
```

**Merchant contact details**
```http
GET /v1/merchants/{merchantHandle}/contact-details
```

Agent notes:
- Treat these endpoints as the canonical merchant bootstrap source for storefront rendering.
- The `requestContext` object tells the shared storefront how to forward merchant context on ordering APIs today.
- `MERCHANT_NOT_FOUND` is a hard stop for merchant bootstrap; do not silently fall back to another merchant.
- For card merchants, `paymentRails.card.availabilityStatus` can be `available`, `needs_onboarding`, `pending`, or `requires_action`. Storefronts must not render card checkout when `available` does not include `card`.

## Partner / Operator Stripe Connect Surface

Partner-authenticated operator routes now cover Stripe Connect onboarding and fee policy management:

- `GET /partner/merchants/{merchantId}/stripe-connect`
- `POST /partner/merchants/{merchantId}/stripe-connect/account`
- `POST /partner/merchants/{merchantId}/stripe-connect/reset`
- `POST /partner/merchants/{merchantId}/stripe-connect/sync`
- `POST /partner/merchants/{merchantId}/stripe-connect/account-links`
- `POST /partner/merchants/{merchantId}/stripe-connect/fee-policy`
- `GET /partner/merchant-activation`

These routes require the partner/admin `X-API-Key`, are not customer-facing, and are intended for operator-assisted merchant onboarding. Axel uses this same path; there is no special-case first-merchant bypass.

Here, partner route `merchantId` is the internal merchant UUID, not the public storefront handle.

## Authenticated Merchant Surface

The authenticated merchant and staff web app now lives on `https://merchant.pizzax402.com` and talks to cookie-backed same-site browser routes under `/merchant/*`.

Key owner-facing routes:

- `POST /merchant/auth/email/intake`
- `POST /merchant/auth/password/sign-in`
- `POST /merchant/auth/password/sign-up`
- `POST /merchant/auth/magic-link/start`
- `POST /merchant/auth/magic-link/consume`
- `POST /merchant/auth/password-reset/start`
- `POST /merchant/auth/password-reset/confirm`
- `GET /merchant/auth/google/start`
- `GET /merchant/auth/google/callback`
- `POST /merchant/auth/profile/complete`
- `POST /merchant/signup`
- `POST /merchant/session/login`
- `POST /merchant/session/logout`
- `GET /merchant/session`
- `GET /merchant/launchpad`
- `PATCH /merchant/launchpad/draft`
- `PATCH /merchant/launchpad/profile`
- `GET /merchant/workspace`
- `PATCH /merchant/workspace`
- `GET /merchant/menu`
- `GET /merchant/ingredients`
- `POST /merchant/ingredients`
- `PATCH /merchant/ingredients/{ingredientId}`
- `POST /merchant/menu/sections`
- `PATCH /merchant/menu/sections/{sectionKey}`
- `POST /merchant/menu/items`
- `PATCH /merchant/menu/items/{sku}`
- `GET /merchant/qr-kit`
- `GET /merchant/payment-hub/stripe-connect`
- `POST /merchant/payment-hub/stripe-connect/account`
- `POST /merchant/payment-hub/stripe-connect/reset`
- `POST /merchant/payment-hub/stripe-connect/sync`
- `POST /merchant/payment-hub/stripe-connect/account-links`
- `POST /merchant/payment-hub/stripe-connect/fee-policy`

Agent notes:
- `pizza-x402-site` remains a public acquisition handoff only; authenticated merchant state should not be implemented there.
- The canonical owner-first auth flow is email-first: `POST /merchant/auth/email/intake` -> Google, password, or magic-link branch -> `POST /merchant/auth/profile/complete` -> Launchpad.
- `POST /merchant/auth/email/intake` is also the auth-capability bootstrap. Auth flow responses include `providers.google`, `providers.magicLink`, and `providers.passwordReset` so the merchant app can hide unavailable methods instead of discovering them through failed clicks.
- Dashboard preferences use `PlatformLocale` only. Keep `uiLocale` to `en` or `fr` even when the merchant storefront content uses `sv-SE` or another `ContentLocale`.
- `POST /merchant/auth/password/sign-up` and first-use social or magic-link flows create an onboarding cookie session immediately, but the owner display name must be completed before Launchpad finalization.
- `GET /merchant/auth/google/start` and `GET /merchant/auth/google/callback` are first-party OAuth browser redirects. The callback must land back on the merchant app continuation route and rely on same-site cookies, not bearer tokens.
- Hosted Google sign-in requires both `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`. The hosted callback URI is `https://api.pizzax402.com/merchant/auth/google/callback`.
- `POST /merchant/signup` and `POST /merchant/session/login` remain compatibility shims for legacy scripts and tests while the unified auth shell migrates to `/merchant/auth/*`.
- Hosted magic-link and password-reset flows require `EMAIL_PROVIDER=resend`, `RESEND_API_KEY`, `ORDER_RECEIPT_FROM_EMAIL`, and `ORDER_PASS_BASE_URL`. Local loopback stacks commonly run with `EMAIL_PROVIDER=noop`; on `.localhost`, the runtime exposes a debug `challengeToken` instead, while hosted `noop` disables those email-based auth paths.
- Merchant browser mutations rely on same-site cookies plus origin checks. Do not build bearer-token or localStorage auth around these routes.
- Stripe Connect onboarding/remediation links should return to `https://merchant.pizzax402.com/launchpad` by default.
- `POST /partner/merchants/{merchantId}/stripe-connect/reset` and `POST /merchant/payment-hub/stripe-connect/reset` are the pre-launch recovery path when a stale Stripe test connected account blocks card checkout. They clear the stored account state and recreate a fresh Stripe test account by default.
- `GET /merchant/workspace` and `PATCH /merchant/workspace` are the canonical Merchant Console profile routes; do not keep binding admin UIs to Launchpad-specific route names.
- `GET /merchant/menu` plus the `POST|PATCH /merchant/menu/*` routes are the canonical Menu Studio control plane; owners and managers can mutate menu data there, while staff must stay out of Merchant Console.
- `GET|POST|PATCH /merchant/ingredients` are the canonical merchant-private ingredient inventory routes for Menu Studio. Ingredient stock hides linked public pizzas and quote validation rejects them, but the public `/v1/menu` response shape does not expose canonical ingredients in this pass.
- `GET /merchant/qr-kit` returns the storefront URL plus printable-placement guidance so QR asset generation can happen without a second backend system.
- `GET /partner/merchant-activation` is the founder-safe visibility surface for signup, Launchpad, Stripe Connect readiness, storefront live, and first paid/fulfilled order milestones. It is merchant-level only and excludes customer PII.

### Tool 1: `get_menu`

Retrieves available menu items with prices.

**HTTP Request:**
```
GET /v1/menu
```

**Response Schema:**
```json
{
  "currency": "EUR",
  "items": [
    {
      "sku": "PZ-MAR",
      "name": "Margherita",
      "description": "Tomato, mozzarella, basil",
      "priceCents": 900,
      "itemType": "pizza",
      "imageUrl": "https://storage.googleapis.com/pizza-x402-artwork/..."
    }
  ],
  "sections": [
    {
      "sectionKey": "pizzas",
      "title": "Pizzas",
      "note": "House favorites first.",
      "sortOrder": 100,
      "items": [
        {
          "sku": "PZ-MAR",
          "name": "Margherita",
          "description": "Tomato, mozzarella, basil",
          "priceCents": 900,
          "itemType": "pizza",
          "sectionKey": "pizzas",
          "imageUrl": "https://storage.googleapis.com/pizza-x402-artwork/..."
        }
      ]
    }
  ]
}
```

**Example curl:**
```bash
curl http://localhost:3000/v1/menu \
  -H "X-Merchant-Handle: merchant-demo-001"
```

**Agent Notes:**
- `priceCents` is an integer in the merchant's quote currency (for Axel, 900 = 9.00 EUR)
- `itemType` is explicit (`pizza`, `drink`, `dessert`, `extra`, `other`) and only `pizza` items use the generative art pipeline
- `imageUrl` is a stable public SVG asset URL when artwork has been generated for the item
- `sections` is additive and preferred for live storefront grouping; keep the top-level `items` fallback for older fixtures and clients
- each section includes `sectionKey`, `title`, optional `note`, `sortOrder`, and the already-filtered `items` rendered in that section
- Only items with `available: true` are returned
- Archived items never appear in the public menu, and unavailable items remain hidden from ordering
- Use `sku` values when creating quotes
- If you are operating a shared storefront, forward the merchant bridge header from bootstrap: `X-Merchant-Handle: <merchantHandle>`

**Possible Errors:**
| Error Code | HTTP | Meaning |
|------------|------|---------|
| `MERCHANT_NOT_FOUND` | 404 | Shared storefront passed an unknown `merchantHandle` |
| `UNAUTHORIZED` | 401 | Deployment requires a valid scoped `X-API-Key` for `/v1/*` routes |
| `RATE_LIMITED` | 429 | Retry after the delay in `Retry-After` / `retryAfterMs` |

---

### Tool 2: `create_quote`

Creates a price quote for an order. Quote includes calculated totals and pickup ETA.

**HTTP Request:**
```
POST /v1/orders/quote
Content-Type: application/json
```

**Request Schema:**
```json
{
  "items": [
    { "sku": "PZ-MAR", "qty": 1 },
    { "sku": "DR-COL", "qty": 2 }
  ],
  "fulfillment": {
    "type": "pickup",
    "window": "2024-01-15T18:00:00Z/2024-01-15T18:30:00Z"
  },
  "contact": {
    "name": "John Doe",
    "phone": "+33612345678",
    "email": "john@example.com"
  },
  "notes": "No olives, please.",
  "paymentMethod": "card"
}
```

**Request Fields:**
- `paymentMethod` (optional): `"card"` or `"crypto"` depending on the merchant's configured rails. For Axel, use `"card"`.
- `contact.email` (required): used for the receipt plus the bookmarkable `Order Pass` recovery link.
- `notes` (optional): pickup notes that survive into the public order status payload.

**Response Schema:**
```json
{
  "quoteId": "uuid-string",
  "currency": "EUR",
  "subtotalCents": 1400,
  "feeCents": 50,
  "totalCents": 1450,
  "lineItems": [
    {
      "sku": "PZ-MAR",
      "name": "Margherita",
      "qty": 1,
      "unitPriceCents": 900,
      "lineTotalCents": 900
    }
  ],
  "pickupETA": "2024-01-15T18:15:00Z",
  "expiresAt": "2024-01-15T18:30:00Z",
  "availablePaymentMethods": ["card"],
  "walletSetupUrl": "https://api.pizzax402.com/v1/wallet-setup",
  "paymentInstructions": "Card payment required: 14.50 EUR. Confirm your order to complete checkout securely by card.",
  "merchantHandle": "au-comptoir-a-patons",
  "merchantAddress": "111 rue Sébastien Gryphe, 69007 Lyon, France",
  "quoteHash": "sha256:...",
  "significance": "medium",
  "riskHints": {
    "unusual": true,
    "reasons": ["above_typical_total"],
    "maxDownsideCents": 1450,
    "recommendedConfirmation": "confirm"
  }
}
```

**Response Fields:**
- `currency`: merchant-bound (`"EUR"` or another Stripe-supported storefront currency for launch card merchants; `"USDC"` only for future/experimental crypto merchants)
- `availablePaymentMethods`: returned from merchant-configured runtime rails
- `paymentInstructions`: card or crypto guidance for the selected quote branch
- `quoteHash`: Deterministic binding hash for this quote snapshot (use in `confirm_order.assertions.expectedQuoteHash`)
- `significance` / `riskHints`: Risk/proportional-friction hints for agent UX and approval policies
- `merchantHandle` / `merchantAddress`: Merchant identity fields for confirm-time assertions

**Example curl:**
```bash
curl -X POST http://localhost:3000/v1/orders/quote \
  -H "Content-Type: application/json" \
  -H "X-Merchant-Handle: merchant-demo-001" \
  -d '{
    "items": [{"sku": "PZ-MAR", "qty": 1}],
    "fulfillment": {"type": "pickup"},
    "contact": {"name": "Agent User", "phone": "+33600000000", "email": "agent@example.com"}
  }'
```

**Agent Notes:**
- Save the `quoteId` — you need it for confirmation
- `totalCents` = `subtotalCents` + `feeCents`
- `pickupETA` is an ISO-8601 timestamp for customer communication
- `expiresAt` indicates when the quote expires (15 minutes from creation)
- `quoteHash` lets you assert you are confirming the exact quote snapshot you reviewed
- `significance` and `riskHints.recommendedConfirmation` are signals for auto-approve vs human confirmation
- `walletSetupUrl` stays in the response for compatibility; storefronts can hide it when crypto is disabled
- `paymentInstructions` provides a brief payment summary; wallet setup links are compatibility fields for future/experimental crypto merchants
- Window is optional; if omitted, pickup is ASAP
- `contact.email` is required so the customer receives a receipt with the bookmarkable `Order Pass` link
- **Important:** Complete payment before `expiresAt` or create a new quote
- **Live deployment note:** read `availablePaymentMethods` from the quote. Axel currently returns `["card"]`.
- **Wallet Check:** only use `walletSetupUrl` when the selected payment branch is crypto
- Shared storefronts should pass `X-Merchant-Handle` so quote pricing, payment rails, hours, and legal/contact parity all stay merchant-bound

**Possible Errors:**
| Error Code | HTTP | Meaning |
|------------|------|---------|
| `INVALID_PAYLOAD` | 400 | Request body doesn't match schema |
| `INVALID_SKU` | 400 | SKU not found in menu |
| `ITEM_UNAVAILABLE` | 400 | Item exists but currently unavailable |
| `MERCHANT_NOT_FOUND` | 404 | Shared storefront passed an unknown `merchantHandle` |
| `UNAUTHORIZED` | 401 | Deployment requires a valid scoped `X-API-Key` for `/v1/*` routes |
| `RATE_LIMITED` | 429 | Retry after the delay in `Retry-After` / `retryAfterMs` |
| `PAYMENT_UNAVAILABLE` | 503 | No payment rails are currently configured/available; operator must fix runtime config |

---

### Tool 3: `confirm_order`

Confirms an order and initiates payment. Behavior depends on payment method:
- **Crypto payments:** First call returns 402 with payment instructions; second call (with payment proof) returns 200 with confirmed order.
- **Card payments:** First call returns 200 with payment intent details. After payment success, a repeated confirm returns 200 with a paid order response (idempotent for repeated confirms).

**HTTP Request:**
```
POST /v1/orders/confirm
Content-Type: application/json
PAYMENT-SIGNATURE: <proof>  (only for crypto payments, on second call)
```

**Request Schema:**
```json
{
  "quoteId": "uuid-string",
  "paymentMethod": "card",
  "assertions": {
    "expectedQuoteHash": "sha256:...",
    "expectedTotalCents": 1450,
    "maxTotalCents": 1450,
    "expectedCurrency": "EUR",
    "expectedPaymentMethod": "card",
    "expectedExpiresAt": "2024-01-15T18:30:00Z",
    "expectedMerchantHandle": "au-comptoir-a-patons",
    "expectedMerchantAddress": "111 rue Sébastien Gryphe, 69007 Lyon, France"
  }
}
```

**Request Fields:**
- `paymentMethod` (optional): `"card"` or `"crypto"` depending on merchant-configured rails.
- `assertions` (optional): Confirm-time intent checks. The server rejects the request if any assertion mismatches (safer agent automation).

**Crypto Payment - First Response (402 Payment Required):**
```json
{
  "paymentRequestId": "abc123-456",
  "amountCents": 1450,
  "currency": "USDC",
  "chain": "eip155:8453",
  "stablecoin": "USDC",
  "instructions": "Use the PAYMENT-REQUIRED header or paymentRequirements to generate a PAYMENT-SIGNATURE header via x402, then resend this request.",
  "walletSetupGuideUrl": "https://api.pizzax402.com/v1/wallet-setup",
  "paymentHelp": "Don't have a wallet? See setup guide: https://api.pizzax402.com/v1/wallet-setup",
  "paymentRequirements": {
    "scheme": "exact",
    "network": "eip155:8453",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "maxAmountRequired": "14.50",
    "payTo": "0x...",
    "maxTimeoutSeconds": 600
  }
}
```

**Crypto Payment - Second Response (200 OK) — after payment:**
```json
{
  "orderId": "uuid-string",
  "status": "paid",
  "currency": "USDC",
  "totalCents": 1450,
  "receipt": {
    "message": "Order paid and sent to kitchen."
  }
}
```

**Example curl (card payment - first call — expect 200 with Checkout Session):**
```bash
curl -X POST http://localhost:3000/v1/orders/confirm \
  -H "Content-Type: application/json" \
  -d '{"quoteId": "YOUR_QUOTE_ID", "paymentMethod": "card"}'
```

Representative response:
```json
{
  "checkoutSessionId": "cs_...",
  "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_...",
  "amountCents": 950,
  "currency": "EUR",
  "status": "open",
  "paymentStatus": "unpaid",
  "expiresAt": "2026-04-09T10:30:00.000Z",
  "instructions": "Complete payment in Stripe Checkout to place this order."
}
```

After the customer completes Stripe Checkout successfully, poll `GET /v1/orders/checkout-sessions/{checkoutSessionId}` until the API returns `ready=true` plus the created `orderId`.

**Example curl (crypto payment - first call — expect 402):**
```bash
curl -X POST http://localhost:3000/v1/orders/confirm \
  -H "Content-Type: application/json" \
  -d '{"quoteId": "YOUR_QUOTE_ID", "paymentMethod": "crypto"}'
```

**Example curl (crypto payment - second call — with payment proof):**
```bash
# For a future/experimental live Coinbase/x402 adapter: generate a base64 payment payload from the
# PAYMENT-REQUIRED header or paymentRequirements, then send it as PAYMENT-SIGNATURE.
# For the local mock adapter only: use paymentRequestId as proof.
curl -X POST http://localhost:3000/v1/orders/confirm \
  -H "Content-Type: application/json" \
  -H "PAYMENT-SIGNATURE: YOUR_BASE64_X402_PAYMENT_PAYLOAD" \
  -d '{"quoteId": "YOUR_QUOTE_ID", "paymentMethod": "crypto"}'
```

**Agent Notes:**

**For Card Payments:**
- First call returns `200` with `checkoutSessionId` and `checkoutUrl`
- Redirect the customer to hosted Stripe Checkout
- Poll `GET /v1/orders/checkout-sessions/{checkoutSessionId}` until the API returns `orderId`
- If the order is not ready yet, keep polling the checkout-session status endpoint while webhook reconciliation completes

**For Crypto Payments:**
- First call returns 402 with `PAYMENT-REQUIRED` header
- `walletSetupGuideUrl` provides link to wallet setup instructions (use if customer needs help)
- `paymentHelp` contains user-friendly message with setup guide link
- For a future/experimental live Coinbase adapter: use `paymentRequirements` or the `PAYMENT-REQUIRED` header to generate a real x402 payment payload
- For the local mock adapter only: set `PAYMENT-SIGNATURE` to the `paymentRequestId`
- Second call (with payment proof) returns 200 with order details
- If customer doesn't have a wallet, direct them to `walletSetupGuideUrl` before retrying payment
- Save the `orderId` for status tracking
- For agentic safety, reuse quote fields in `assertions` to prevent silent divergence (amount/method/expiry/merchant)

**Possible Errors:**
| Error Code | HTTP | Meaning |
|------------|------|---------|
| `INVALID_PAYLOAD` | 400 | Request body doesn't match schema |
| `QUOTE_NOT_FOUND` | 400 | Quote expired or doesn't exist |
| `ASSERTION_FAILED_*` | 400 | A confirm assertion (quote hash/amount/method/currency/expiry/merchant) did not match |
| `UNAUTHORIZED` | 401 | Deployment requires a valid scoped `X-API-Key` for `/v1/*` routes |
| `PAYMENT_REQUIRED` | 402 | Payment not yet received |
| `PAYMENT_INVALID` | 402 | Payment proof invalid or expired |
| `RATE_LIMITED` | 429 | Retry after the delay in `Retry-After` / `retryAfterMs` |
| `PAYMENT_UNAVAILABLE` | 503 | No payment rails are currently configured/available; operator must fix runtime config |

Representative error payloads:
```json
{
  "error": "Quote assertion failed: total 1450c exceeds max 1400c.",
  "errorCode": "ASSERTION_FAILED_TOTAL",
  "retryable": false
}
```
```json
{
  "error": "No payment rail is currently configured for the requested merchant or payment method.",
  "errorCode": "PAYMENT_UNAVAILABLE",
  "retryable": false
}
```

---

### Tool 4: `get_order_status`

Retrieves current status and details of an order.

**HTTP Request:**
```
GET /v1/orders/{orderId}
```

**Response Schema:**
```json
{
  "id": "uuid-string",
  "status": "paid",
  "paymentMethod": "card",
  "currency": "USDC",
  "totalCents": 1450,
  "items": [
    {
      "sku": "PZ-MAR",
      "name": "Margherita",
      "qty": 1,
      "unitPriceCents": 900
    }
  ],
  "contact": {
    "verified": true
  },
  "fulfillment": {
    "type": "pickup",
    "window": "2024-01-15T18:00:00Z/2024-01-15T18:30:00Z"
  },
  "notes": "No olives, please.",
  "pickupETA": "2024-01-15T18:15:00Z",
  "etaStatus": "provisional",
  "pickupCode": "440000",
  "acknowledgedAt": "2024-01-15T17:55:00Z",
  "readyAt": "2024-01-15T18:12:00Z",
  "fulfilledAt": null
}
```

**Example curl:**
```bash
curl http://localhost:3000/v1/orders/YOUR_ORDER_ID \
  -H "X-Merchant-Handle: merchant-demo-001"
```

**Agent Notes:**
- Status values: `paid` → `acknowledged` → `ready` → `fulfilled`
- Use `pickupETA` to inform the customer when their order will be ready
- `acknowledgedAt` indicates kitchen has seen the order
- `readyAt` indicates the order is ready for pickup
- `pickupCode` is a non-blocking pickup trust signal shown to both customer and staff
- `notes` surfaces the customer pickup notes from the original quote; no customer account or recovery token is required in v1
- `fulfilledAt` indicates customer has picked up

**Possible Errors:**
| Error Code | HTTP | Meaning |
|------------|------|---------|
| `ORDER_NOT_FOUND` | 404 | Order ID not found |
| `UNAUTHORIZED` | 401 | Deployment requires a valid scoped `X-API-Key` for `/v1/*` routes |
| `RATE_LIMITED` | 429 | Retry after the delay in `Retry-After` / `retryAfterMs` |

---

### Tool 5: `get_wallet_setup`

Retrieves step-by-step instructions for setting up a Base wallet with USDC for future/experimental x402 payments. Launch customers should not need this tool because launch checkout is Stripe-only.

**HTTP Request:**
```
GET /v1/wallet-setup
```

**Response Schema:**
```json
{
  "title": "Wallet Setup Guide for USDC on Base",
  "description": "Step-by-step instructions to get USDC and set up a Base wallet for pizza payments",
  "estimatedTime": "5-10 minutes",
  "steps": [
    {
      "step": 1,
      "title": "Choose a Wallet",
      "description": "Select a wallet that supports Base network and USDC",
      "options": [
        {
          "name": "Coinbase Wallet",
          "recommended": true,
          "description": "Easiest for beginners, built-in USDC support",
          "links": [
            {
              "label": "Download Coinbase Wallet",
              "url": "https://www.coinbase.com/wallet",
              "type": "official"
            }
          ]
        }
      ]
    }
  ],
  "network": {
    "name": "Base",
    "chainId": "eip155:8453",
    "chainIdNumeric": 8453,
    "currency": "USDC",
    "nativeCurrency": "ETH",
    "assetAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "rpcUrl": "https://mainnet.base.org",
    "explorer": "https://basescan.org/"
  },
  "resources": {
    "thisEndpoint": "http://localhost:3000/v1/wallet-setup",
    "baseNetwork": "https://docs.base.org/",
    "coinbaseWallet": "https://www.coinbase.com/wallet",
    "baseBridge": "https://bridge.base.org/"
  },
  "quickStart": {
    "summary": "Fastest path: Download Coinbase Wallet → Buy USDC → Pay",
    "steps": [
      "Download Coinbase Wallet from coinbase.com/wallet",
      "Buy USDC in the Coinbase app",
      "Send USDC to your Coinbase Wallet on Base network",
      "Ready to pay!"
    ]
  },
  "support": {
    "note": "If you need help, contact the merchant or visit the links above",
    "merchantAddress": "111 rue Sébastien Gryphe, 69007 Lyon, France"
  }
}
```

**Example curl:**
```bash
curl http://localhost:3000/v1/wallet-setup \
  -H "X-Merchant-Handle: merchant-demo-001"
```

**Agent Notes:**
- Do not use this endpoint in the launch checkout path. It is for future/experimental x402 flows only.
- Use this endpoint only when an explicitly crypto-enabled experimental merchant needs help setting up a wallet.
- The response includes structured steps with links to official resources
- **Use `quickStart.steps`** for a simplified summary to present to users (4 simple steps)
- Recommend Coinbase Wallet for beginners (marked as `recommended: true`)
- Always verify the customer has USDC on Base network before payment
- `network` object provides all technical details needed for wallet configuration
- `resources.thisEndpoint` is the canonical URL for this guide

**When to use:**
- A future/experimental x402 flow is explicitly enabled.
- A crypto payment fails with network/balance errors.
- A customer in that experimental flow is new to crypto payments.
- Do not use it for Stripe launch card checkout.

**Possible Errors:**
| Error Code | HTTP | Meaning |
|------------|------|---------|
| `UNAUTHORIZED` | 401 | Deployment requires a valid scoped `X-API-Key` for `/v1/*` routes |
| `RATE_LIMITED` | 429 | Retry after the delay in `Retry-After` / `retryAfterMs` |

---

## Wallet Setup Guide (Future/Experimental x402 Only)

Launch customers do not need a wallet. In future/experimental x402 flows, customers need:
1. A crypto wallet (Coinbase Wallet or MetaMask recommended)
2. USDC on Base network (not Ethereum mainnet)
3. Enough USDC to cover order total + network fees

**Quick Setup Path (Recommended):**
1. Download Coinbase Wallet: https://www.coinbase.com/wallet
2. Buy USDC in Coinbase app
3. Send USDC to Coinbase Wallet on Base network
4. Ready to pay!

**Detailed Guide:** See `GET /v1/wallet-setup` endpoint or visit the documentation URL in the response.

**Common Issues:**
- Customer has USDC on Ethereum mainnet → Need to bridge to Base
- Customer doesn't have enough USDC → Need to buy more
- Wrong network selected → Must use Base network (eip155:8453)

---

## Complete Conversation Flow Example

Here's how an LLM agent should orchestrate a pizza order:

### Step 1: User requests menu
**User:** "I'd like to order a pizza from Au Comptoir à Pâtons."

**Agent action:** Call `GET /v1/menu`

**Agent response:** "Here's the menu at Au Comptoir à Pâtons:
- Margherita — 10.00 EUR
- Funghi — 12.00 EUR
- Reine — 14.00 EUR
- Diavola — 15.00 EUR

What would you like to order?"

### Step 2: User builds cart
**User:** "One Margherita please."

**Agent action:** Call `POST /v1/orders/quote` with:
```json
{
  "items": [
    { "sku": "PZ-MAR", "qty": 1 }
  ],
  "fulfillment": { "type": "pickup" },
  "contact": {
    "name": "User Name",
    "phone": "+33600000000",
    "email": "user@example.com"
  },
  "paymentMethod": "card"
}
```

**Agent response:** "Your order:
- 1x Margherita — 10.00 EUR
- Service fee — 0.50 EUR
- **Total: 10.50 EUR**

Ready for pickup at approximately 18:15. 

**Payment:** Au Comptoir à Pâtons is currently card-default. I'll open hosted Stripe Checkout now, then I'll confirm the order once payment succeeds."

### Step 3: User confirms
**User:** "Yes, confirm it."

**Agent action:** Call `POST /v1/orders/confirm` with `{ "quoteId": "...", "paymentMethod": "card" }`

**Agent receives 200 response** with `checkoutSessionId` and `checkoutUrl`.

**Agent action:** Redirect the customer to hosted Stripe Checkout.
**Agent action:** Poll `GET /v1/orders/checkout-sessions/{checkoutSessionId}` until it returns `orderId`
**Agent response:** "Order confirmed! Order ID: `550e8400-e29b-41d4-a716-446655440000`. Your Margherita will be ready for pickup at 111 rue Sébastien Gryphe, 69007 Lyon, France at approximately 18:15. You'll receive status updates as the kitchen acknowledges and prepares the order."

### Step 4: User checks status (optional)
**User:** "What's the status of my order?"

**Agent action:** Call `GET /v1/orders/{orderId}`

**Agent response:** "Your order is currently `acknowledged` — the kitchen is preparing it. Pickup ETA remains 18:15. You should have received an SMS notification when the kitchen started preparing your order."

---

## Error Handling for Agents

All errors follow a consistent structure:

```json
{
  "error": "Human-readable message",
  "errorCode": "MACHINE_READABLE_CODE",
  "details": { "sku": "INVALID-SKU" }
}
```

**Action table for agents:**

| Error Code | Recommended Action |
|------------|-------------------|
| `INVALID_PAYLOAD` | Check request format, retry with corrected data |
| `INVALID_SKU` | Re-fetch menu, use valid SKU |
| `ITEM_UNAVAILABLE` | Inform user, suggest alternative items |
| `QUOTE_NOT_FOUND` | Quote expired, create a new quote |
| `ORDER_NOT_FOUND` | Verify order ID, may have been fulfilled |
| `RATE_LIMITED` | Pause retries until the `Retry-After` / `retryAfterMs` delay expires |
| `PAYMENT_REQUIRED` | Payment pending, process payment and retry |
| `PAYMENT_INVALID` | Payment failed, inform user, may need new quote |
| `PAYMENT_UNAVAILABLE` | Stop and report service misconfiguration; ask operator to enable at least one payment rail |
| `ASSERTION_FAILED_QUOTE_HASH` | Re-run quote review; quote snapshot changed or mismatched |
| `ASSERTION_FAILED_TOTAL` | Reconfirm amount with user; create a new quote if needed |
| `ASSERTION_FAILED_METHOD` | Use the expected payment method or ask user to confirm the switch |
| `ASSERTION_FAILED_CURRENCY` | Reconfirm payment branch (card vs crypto) and expected currency |
| `ASSERTION_FAILED_EXPIRY` | Quote metadata changed/expired; request a new quote |
| `ASSERTION_FAILED_MERCHANT` | Stop and verify merchant identity before proceeding |

---

## OpenAPI Specification

Full OpenAPI 3.1 spec available at:
- Runtime: `GET /v1/openapi.yaml`
- File: `openapi.yaml`

Use this for schema validation and code generation.

## LLM Bundle Artifacts

Agent-focused bundle artifacts are generated in `docs/public/`:
- `docs/public/llms.txt` (stable high-value docs path index)
- `docs/public/llms-full.md` (concatenated docs bundle with source delimiters)

Generation commands:
```bash
cd ../pizza-x402-api
node scripts/docs/generate-llms-bundle.mjs
node scripts/docs/generate-llms-bundle.mjs --check
```

---

## MCP Server Package (Local Agent Wrapper)

Pizza x402 ships an official MCP server repo for builders and agents:

- Official public repo: `https://github.com/pizzax402/pizzax402-mcp`
- Runtime-side implementation source in `pizzax402/pizza-x402-api`: `packages/mcp-server`
- Core tools: `get_menu`, `create_quote`, `confirm_order`, `get_order_status`, `get_wallet_setup`

Useful commands:

```bash
cd ../pizza-x402-api
npm run mcp:build
npm run mcp:test
npm run mcp:test:http
npx tsx packages/mcp-server/src/index.ts
npx tsx packages/mcp-server/src/index.ts --http
```

If you are consuming the official standalone repo instead of this monorepo, use the commands documented in `pizzax402-mcp/README.md`.

Configuration (env vars):
- `PIZZA_X402_BASE_URL` (default: `http://localhost:3000`)
- `PIZZA_X402_TIMEOUT_MS` (default: `10000`)
- `PIZZA_X402_PARTNER_API_KEY` (required for partner tools: `list_partner_orders`, `ack_order`, `fulfill_order`)
- `PIZZA_X402_MCP_HTTP_HOST` / `PIZZA_X402_MCP_HTTP_PORT` / `PIZZA_X402_MCP_HTTP_PATH` (for HTTP transport mode)

See `docs/public/mcp.md` for usage notes, repo roles, and MCP-specific behavior.

---

## A2A Gateway Package (Agent-to-Agent REST + JSON-RPC Subset)

Pizza x402 also ships an A2A-style gateway package for task-oriented orchestration over HTTP:

- Package in `pizzax402/pizza-x402-api`: `packages/a2a-gateway`
- Agent Card: `GET /.well-known/agent-card.json`
- REST task endpoints: `POST /a2a/messages`, `POST /a2a/messages/stream`, `GET /a2a/tasks`, `GET /a2a/tasks/{id}`, `POST /a2a/tasks/{id}/cancel`, `GET /a2a/tasks/{id}/events`
- JSON-RPC endpoint: `POST /a2a/jsonrpc` (JSON-RPC `2.0`)
- Supported A2A version header (Sprint 6 subset): `A2A-Version: 0.2`
- Multi-turn context correlation: optional `contextId` (task metadata carries context snapshots)

Useful commands:

```bash
cd ../pizza-x402-api
npm run a2a:build
npm run a2a:test
npm run a2a:test:conformance
npx tsx packages/a2a-gateway/src/index.ts
```

Configuration (env vars):
- `PIZZA_X402_BASE_URL` (default: `http://localhost:3000`)
- `PIZZA_X402_TIMEOUT_MS` (default: `10000`)
- `PIZZA_X402_A2A_HOST` / `PIZZA_X402_A2A_PORT`
- `PIZZA_X402_A2A_PATH_PREFIX` (default: `/a2a`)
- `PIZZA_X402_A2A_PUBLIC_URL` (optional; used in Agent Card endpoint URLs)

See `docs/public/a2a.md` for the Sprint 6 subset contract (REST, JSON-RPC, SSE streaming, and context correlation examples).

---

## Launch vs Experimental Payments

| Aspect | Launch (Stripe) | Experimental x402 |
|--------|-----------------|-------------------|
| `X402_ADAPTER` | `disabled` | `mock` locally or `coinbase` for explicitly approved experiments |
| Card rail | Stripe Checkout Session flow for merchants with Connect ready | Optional compatibility path only |
| Payment proof | Stripe Checkout plus webhook reconciliation | Use x402 SDK with `paymentRequirements`; local mock can use `paymentRequestId` as signature |
| Network | N/A | Base (eip155:8453) |
| Asset | Merchant Stripe currency | USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) |

---

## Merchant Info

**Current merchant:** Au Comptoir à Pâtons
**Address:** 111 rue Sébastien Gryphe, 69007 Lyon, France
**Fulfillment:** Pickup only
**Min prep time:** 7 minutes
