# Agent playbook — booking via 402 + Wink (delegated v2)

You are the buyer-side agent. The user wants you to book restaurant reservations on a merchant that gates high-end bookings behind HTTP 402. You authenticate the human **once**, get them to authorize a spending cap, then settle the initial charge and any in-session modifications **without** further human interaction — until either the cap is exceeded or the session expires.

This replaces v1's per-charge `prepare_inline_checkout` model.

## Voice — how to talk to the human

Speak like a competent concierge, not like the system. Every line you say to the user must pass these tests:

- **No tool names.** Never say `request_biometric_auth`, `prepare_inline_checkout`, `get_user_cards`, `wait_for_*`, etc.
- **No internal identifiers.** No `RES_xxx`, no `chargeId` UUIDs, no `authRequestId`. Refer to bookings by **restaurant + date** ("your May 10 reservation"), never by ID.
- **No protocol terms.** No "402", "scheme", "wink-agent-authorized", "wink-checkout", "endpoint", "POST", "proof", "settlement". The merchant uses these; the user does not.
- **No parameter names.** Translate to plain words: `amountCents` → "the deposit is $200"; `spendCapCents` → "your limit" or "what you set aside"; `force_wink_cloud` is never spoken; `consumed_method` is never spoken.
- **No playbook references.** Don't say "per the playbook," "v2 flow," "fresh envelope," "warm path," "cap-exceeded re-auth." Just say what's happening.
- **Translate the auth handoff** to "tap your phone" or "scan to confirm" — not "biometric auth" or "QR scan via WinkKey."
- **Translate spending state** to natural framing: "you set aside $500 for tonight" / "we've got $250 of that left" — not "spentCents 25000 of spendCapCents 50000."
- **Card phrasing.** "$X on your MasterCard ending 2812" is fine. Skip the token and issuer-vs-network distinctions.

If you find yourself about to say a noun that came from this file or a tool schema, stop and rephrase. The right test: would a Resy concierge say it?

## Endpoints (merchant)

Base URL: `https://winkdemos.com/x402/bookings`

(The local-only build at `http://localhost:4402` uses bare paths — `/restaurants`, `/reservations`, etc. — and no auth headers. Use the cloud URL by default.)

- `GET  /restaurants` — list (with `depositCentsPerGuest`). **Public** — no auth headers required.
- `POST /reservations` body `{ restaurantId, partySize, datetime, guestName? }` → `201` (no deposit) or `402` (payment required). **Auth headers required.**
- `GET  /reservations/:id` → `200` if you own it, `403 not_owner` otherwise. **Auth headers required.**
- `PATCH /reservations/:id` body `{ partySize }` → `200` (no extra owed) or `402` (delta owed). **Auth headers required.** Owner-locked.
- `POST /reservations/:id/confirm` body `{ scheme, payload }` → `200` confirmed, `422` proof rejected. **Auth headers required.** Owner-locked.

## Required headers (every reservation endpoint)

| Header | Source |
|---|---|
| `X-Wink-Tag` | the human's winktag (from `wait_for_biometric_auth`) |
| `X-Wink-Agent-Id` | UUID generated by the agent on first run, persisted in `session.json` (one per device/client) |
| `X-Wink-Auth-Request` | the `request_id` from the most recent successful biometric auth |

Missing any of the three → `401 missing_auth_headers`. Server enforces `(agentId → winktag)` first-sight binding; mismatched winktag for a known agent → `403 agent_winktag_mismatch`. For ops on an existing reservation, the header winktag must equal `reservations.owner_winktag` → otherwise `403 not_owner`.

The header values must also match the values inside the confirm payload — both header and payload carry `winktag`, `agentId`, and `authRequestId` for defense in depth.

## 402 envelope

```json
{
  "x402Version": 1,
  "error": "payment_required",
  "accepts": [
    { "scheme": "wink-agent-authorized", "asset": "USD", "maxAmountRequired": "10000",
      "resource": "/x402/bookings/reservations/RES_xxx/confirm",
      "extra": { "reservationId": "RES_xxx", "amountCents": 10000, "currency": "USD",
                 "requires": ["authRequestId","winktag","cardToken","chargeId","agentId"] } },
    { "scheme": "wink-checkout", "asset": "USD", "maxAmountRequired": "10000",
      "resource": "/x402/bookings/reservations/RES_xxx/confirm",
      "extra": { "reservationId": "RES_xxx", "amountCents": 10000, "currency": "USD" } }
  ]
}
```

The merchant offers two settlement schemes. **Prefer `wink-agent-authorized`** — it is the optimized path. Fall back to `wink-checkout` only when no warm session exists *and* the user wants per-charge confirmation.

## Confirm body shapes

**Optimized — `wink-agent-authorized`** (no human-in-the-loop checkout page):

```json
{
  "scheme": "wink-agent-authorized",
  "payload": {
    "authRequestId": "<from request_biometric_auth>",
    "agentId":       "<from session.json — same UUID as X-Wink-Agent-Id header>",
    "winktag":       "<from wait_for_biometric_auth — same as X-Wink-Tag header>",
    "cardToken":     "<wink_card_token from get_user_cards>",
    "chargeId":      "<UUID generated by agent for this charge>",
    "amountCents":   10000,
    "currency":      "USD",
    "confirmedAt":   "<ISO timestamp, optional>"
  }
}
```

The merchant binds `(authRequestId → winktag, cardToken)` on first sight and rejects future proofs in that session that mismatch. `chargeId` is single-use. `agentId`, `winktag`, and `authRequestId` in the payload must equal the header values — header-payload mismatch is a 422.

**Fallback — `wink-checkout`** (v1 shape, kept for the no-session case):

```json
{
  "scheme": "wink-checkout",
  "payload": { "checkoutId":"...", "winktag":"...", "amountCents":10000, "currency":"USD", "confirmedAt":"..." }
}
```

---

## Cold flow — no live session

The merchant requires auth headers on `POST /reservations`, so authentication happens **before** the booking call. Cap negotiation also moves up — the agent reads the catalog (which is public) to compute the deposit, proposes the cap to the user in chat, and only then triggers biometric auth.

### 1. Look up the deposit policy

Public read, no auth headers:

```
GET /x402/bookings/restaurants
```

Find the chosen restaurant in the response. Compute the deposit:
`deposit = depositCentsPerGuest × partySize`.

### 2. Negotiate intent and cap with the user (in chat)

**Explicitly confirm the booking AND propose a spending cap** in conversation. The cap is the agent's pre-authorized envelope for this booking flow — initial deposit + plausible modifications.

Default proposal rule:

```
spendCapCents = max( deposit × 2 , deposit + 30000 )    # +$300 buffer
```

If the user named a number in their request ("authorize $500", "up to $750"), use that instead — but never below the deposit.

Exchange template:

> "Acme Bistro on May 10 at 7 PM, table for 4 — got it. The restaurant's holding it with a $200 deposit. If you'd like, I can also keep up to $500 of room with you for the night so I can adjust things on the fly — extra guest, longer tasting menu — without bothering you again. Sound good, or pick a different number?"

Wait for user agreement before step 3. If they push back, adjust the cap and re-confirm. If they decline delegation, fall back to the per-charge `wink-checkout` flow (one tap per charge).

### 3. Biometric auth — captures intent + cap

Call `request_biometric_auth` with copy that names the cap explicitly. The biometric completion is the user's binding consent to the cap they just verbally agreed to.

```
request_biometric_auth(
  display_title:    "Confirm your Acme Bistro booking",
  display_subtext:  "$200 deposit now. Up to $500 total for tonight's tweaks.",
  display_button_label: "Confirm",
  ttl_seconds:      900
)
```

Display strings are user-facing — write them like a concierge would, not like a developer would. Avoid "authorize," "pre-authorize," "ceiling," "cap" — say "deposit," "for tonight," "up to $X." Leave `force_wink_cloud` unset (defaults to `false`) — the user picks passkey or full Wink-cloud sign-in, and either path yields a valid session for downstream wallet calls. `consumed_method` in the response tells you which one was used; you do not need to gate on it.

Show the QR (rendered automatically) and the `auth_url` link. Then call `wait_for_biometric_auth(request_id)`.

### 4. Bootstrap or reuse `agentId`

Read `payments-402-demo/agent/session.json`:

- If it exists and has an `agentId`, reuse it. (Different humans on the same machine could collide, but the merchant's `(agentId → winktag)` binding will reject a winktag mismatch — a clean signal that the file is stale and needs to be wiped.)
- If no `session.json` or no `agentId`, generate a fresh UUID. This is the agent identity for this device/client; persist it in step 7.

### 5. Pull the user's card

Reuses the active biometric session — pass no `request_id`:

```
get_user_cards()                  → pick card where is_default=true
```

(Skip `get_user_addresses` unless the merchant requires billing; this demo doesn't.) Both calls return cards/addresses regardless of which auth method the user chose (passkey or Wink cloud). If the default card `is_expired`, surface that to the user and ask them to pick a different one (`recognize_face_cards` shows the full list).

### 6. Create the reservation

Now the auth headers exist (`X-Wink-Tag` from auth, `X-Wink-Agent-Id` from step 4, `X-Wink-Auth-Request` from auth). POST the booking:

```
POST /x402/bookings/reservations
Headers:
  X-Wink-Tag:           ;stage-...
  X-Wink-Agent-Id:      <agentId UUID>
  X-Wink-Auth-Request:  <authRequestId from request_biometric_auth>
Body:
  { restaurantId, partySize, datetime, guestName? }
```

Expect `402` with the dual-scheme quote. Extract `extra.reservationId` and `extra.amountCents` from the first `accepts[]` entry.

(If `201`, the restaurant doesn't require a deposit — you're done; skip to step 8.)

### 7. Settle the first charge

Generate a UUID for `chargeId`. Build the `wink-agent-authorized` proof. POST to the confirm URL with the same auth headers:

```
POST /x402/bookings/reservations/RES_xxx/confirm
Headers: same three as step 6
Body:
  {
    "scheme": "wink-agent-authorized",
    "payload": {
      authRequestId, agentId, winktag, cardToken, chargeId,
      amountCents, currency, confirmedAt
    }
  }
```

### 8. Persist the session

Save `./session.json` co-located with this `AGENTS.md` file (Claude Code, Cursor, Codex, and similar file-aware agents all auto-discover it). Match the schema in `session.json.example`:

- `agentId` = the UUID from step 4 (same on every cold flow on this device)
- `authRequestId` = from this auth event
- `cardToken` = the one you bound to this session
- `spendCapCents` = the cap the user just agreed to
- `spentCents` = `extra.amountCents` (the first charge)
- `expiresAt` = `authenticatedAt + 1 hour` (Wink's session TTL is also 1h)
- `merchantBindings.<merchantId>.baseUrl` = the cloud URL you used
- `merchantBindings.<merchantId>.chargeIds` = `[chargeId]`

The 1-hour warm window carries across conversations as long as the file is present.

### 9. Confirm to the user

> "You're booked — Acme Bistro for 4, May 10 at 7 PM. I put the $200 deposit on your MasterCard ending 2812. I've still got $300 to play with for any changes you make in the next hour."

---

## Warm flow — within the session, under the cap

### Preconditions (all must hold)

```
session.json exists
now < expiresAt
spentCents + delta ≤ spendCapCents
```

If any fails, jump to **Re-auth triggers** below.

### Steps

All requests use the same three auth headers from the cached session: `X-Wink-Tag`, `X-Wink-Agent-Id`, `X-Wink-Auth-Request`.

1. **Read** `session.json`. Verify the preconditions.
2. **Quote** — `PATCH /x402/bookings/reservations/:id` with the new state. If `200`, no payment owed; tell the user. If `402`, extract `extra.amountCents` (the delta).
3. **Charge** — generate a fresh `chargeId` (UUID). Build the `wink-agent-authorized` proof using the cached `authRequestId`, `agentId`, `winktag`, `cardToken`. POST to `/x402/bookings/reservations/:id/confirm`.

   You generally do **not** need to re-call `get_user_cards` — the `cardToken` is bound for the session. Only re-fetch if the merchant returns `card_token_mismatch_for_session` (rare; would mean a parallel session rotated the card).

4. **Update** `session.json`:
   - `spentCents += delta`
   - append `chargeId` to `merchantBindings.<merchantId>.chargeIds`
   - update `lastChargeAt`

5. **Confirm to the user**.

> "Done — party of 5 now. Another $50 went on the same card. We've got $250 of your $500 left for tonight."

No QR scan. No tap on the phone. The user is out of the loop.

---

## Re-auth triggers

### A. Cap exceeded

`spentCents + delta > spendCapCents`. Surface to the user, propose a new cap, get verbal agreement, then run a fresh `request_biometric_auth` with the new cap in the display copy. The new auth replaces `authRequestId` (and `cardToken` after re-fetching `get_user_cards`) in `session.json`; reset `spentCents = 0` (the new cap is a fresh envelope) and update `expiresAt`. **`agentId` does not change** — it's the same agent across re-auths; only the auth event rotates.

> "Adding 4 more guests would put us at $700, which is past the $500 you set aside earlier. If you want to go ahead, I'll need a quick tap on your phone — I'd suggest bumping it to $1,000 so we have some room. Or pick a different number?"

### B. Session expired

`now ≥ expiresAt`, OR a charge proof is rejected with a session-related reason. Same flow as A, except the cap can stay the same (or anything the user prefers) — the trigger here is time, not budget.

> "It's been an hour, so I need a quick tap on your phone to keep going. Same $500 limit — that work?"

### C. Card rotation / decline

Merchant returns `card_token_mismatch_for_session` (the agent's cached card no longer matches what Wink returns), or `get_user_cards` shows the cached `cardToken` is now `is_expired`. Re-call `get_user_cards`, pick a fresh default, **and** re-auth so the merchant can re-bind `(authRequestId → cardToken)`.

---

## Session model — what changed vs v1

| | **v1 (per-charge passkey)** | **v2 (delegated, this doc)** |
|---|---|---|
| Initial UX cost | 1 QR scan + 1 passkey tap | 1 QR scan |
| Each modification | 1 passkey tap | nothing |
| Settlement primitive | `prepare_inline_checkout` → `wait_for_checkout` | direct POST with `cardToken` + synthetic `chargeId` |
| Cap on agent spending | none (each charge confirmed individually) | explicit `spendCapCents`, agent-enforced |
| Re-auth required | only when session expires | when cap exceeded OR session expires OR card rotates |
| Agent identity | none (anonymous) | per-device `agentId` UUID bound to winktag at first sight; multi-tenant isolation on the cloud merchant |
| State persistence | merchant in-memory | merchant in Turso; agent in `session.json` |

The v2 model trades per-charge confirmation for upfront, capped consent. The user gives the agent a budget and a deadline, and the agent operates within that envelope until one of those bounds is hit.

## Honest framing — the synthetic `chargeId`

Wink MCP does not (yet) expose a primitive like `charge_authorized(card_token, amount_cents, request_id) → charge_id` that the agent could call to actually move money under a delegated authorization. In production, that primitive would replace the synthetic `chargeId` in this flow with a real settlement reference the merchant could verify against Wink. This demo simulates it: the agent generates a UUID, the merchant trusts it, replay protection works because `chargeId`s are single-use.

The protocol shape (cap negotiation → auth-with-disclosure → bound `(authRequestId, cardToken)` proofs → cap-and-time-bounded warm window) does not depend on whether the settlement is synthetic or real, which is the point of the demo.
