---
name: rate-booking
description: The booking receipt for your agent. Real appointments at any public booking page, paid via x402.
homepage: https://www.rateai.dev
metadata:
  version: 3
---

# Rate — The booking outcome layer for AI agents

Rate is the **receipt** your agent hands the user — not a browser session, not raw HTML.

You send a public booking URL (Square, Fresha, Vagaro, Booksy, Acuity, Calendly, and more), Rate handles the booking on your behalf, and you get back a **durable booking record** with a parsed confirmation, payment proof (`tx_hash`), and an audit trail. Booking costs **$0.25 USDC** via x402 on Base. No merchant onboarding, no OAuth tokens.

## What you get

- Durable booking record + `receipt_id`
- Parsed `confirmation_data` (referenceId, manageUrl) — typed, not a screenshot
- Idempotent retries (same `Idempotency-Key` → same booking, never double-charges)
- Slot reservation holds (10 min, opaque slot_id) so multiple agents don't race
- `POST /api/bookings/:id/cancel` lifecycle — free in v0
- Push events on `booking.confirmed` / `booking.failed` / `booking.cancelled` via webhooks
- Pay-per-confirmed-booking ($0.25) via x402 — no monthly subscription

## Auth — pay $0.25 USDC per booking via x402

Only `POST /api/book` costs money. Everything else (inspect, availability, cancel, webhooks, history, traces) is free.

You pay through your **wallet skill**. Rate doesn't ship a wallet — instead, you load any x402-aware wallet skill alongside this one, and that skill handles the payment when our server returns a 402.

### Supported wallet skills

If you've installed `coinbase/agentic-wallet-skills`, the `pay-for-service` skill auto-triggers when Rate returns 402 — you don't need to invoke it explicitly. Other wallet skills behave similarly.

| Wallet | Setup (one-time) | How the agent pays |
| --- | --- | --- |
| **Awal** (Coinbase Agentic Wallets) | `npx skills add coinbase/agentic-wallet-skills` (then sign in via the `authenticate-wallet` skill — the agent walks you through email OTP) | invoke the `pay-for-service` skill (auto-triggers on 402); under the hood it runs `npx awal@2.8.2 x402 pay …` |
| **Tempo** | Read `https://tempo.xyz/SKILL.md`, fund with USDC | `tempo request POST .../api/book …` |
| **AgentCash** | `npx agentcash@latest onboard`, fund the wallet | `npx agentcash fetch .../api/book …` |
| Any x402 wallet | follow that wallet's setup skill | invoke its x402 / `pay-for-service` primitive against `/api/book` |

### How the 402 handshake works

1. You `POST /api/book` without payment. Server returns **HTTP 402**.
2. Two places carry the canonical x402 v2 `PaymentRequired` body:
   - `PAYMENT-REQUIRED` response header (base64-encoded — preferred for v2 clients)
   - JSON response body (mirrored for human / curl debugging)
3. Hand the request to your wallet skill's payment primitive. It parses the challenge, signs an EIP-712 USDC `transferWithAuthorization`, and retries with `PAYMENT-SIGNATURE` (v2) or `X-PAYMENT` (v1) — Rate accepts both.
4. **Pay only on success.** Rate verifies your signature off-chain, runs the booking, and only then settles on-chain. If the booking fails (`needs_human` / `failed`), no on-chain transfer happens — the authorization just expires.
5. On success, the `PAYMENT-RESPONSE` (v2) / `X-PAYMENT-RESPONSE` (v1) response header carries the settlement result with the real `tx_hash`.

### Network details (returned on every 402)

- `network`: CAIP-2 — `eip155:8453` (Base mainnet) or `eip155:84532` (Base Sepolia testnet)
- `asset`: USDC ERC-20 contract address on the selected chain
- `amount`: atomic units (USDC has 6 decimals; $0.25 = `250000`)
- `payTo`: Rate's receiving address
- `maxTimeoutSeconds`: how long the authorization stays valid (10 min — covers our ~3 min booking flow)

## Endpoints

| Method | Path                          | Cost      | Purpose                                      |
| ------ | ----------------------------- | --------- | -------------------------------------------- |
| POST   | `/api/inspect`                | free      | detect platform, business, risk flags        |
| POST   | `/api/availability`           | free      | extract available slots on the live page     |
| POST   | `/api/book`                   | 0.25 USDC | book the appointment, return durable receipt |
| GET    | `/api/bookings/:id`           | free      | fetch booking + parsed confirmation + trace  |
| POST   | `/api/bookings/:id/cancel`    | free (v0) | cancel a booked appointment                  |
| POST   | `/api/webhooks`               | free      | register a webhook for booking events        |
| GET    | `/api/traces/:id`             | free      | fetch full event trace                       |

## Workflow

Execute step by step. Do not batch or preview.

### 1. Inspect the booking page

```
POST /api/inspect
Content-Type: application/json

{ "booking_url": "https://book.squareup.com/appointments/..." }
```

Returns `{ supported, platform, business_name, risk_flags, trace_id, next_step }`.

**If `supported` is false**, the page requires human intervention. Do not proceed — inform the user.

### 2. Check availability

```
POST /api/availability
Content-Type: application/json

{
  "booking_url": "https://...",
  "service_query": "haircut",
  "date": "2026-05-03",
  "time_window": { "start": "09:00", "end": "18:00" }
}
```

Returns `{ business_name, platform, service, slots: [{ slot_id, start_time, display, confidence }], trace_id, next_step }`.

`slot_id` is opaque and **expires in 10 minutes**. Pass it unchanged to `/api/book`. Other agents looking at the same calendar see the same slot — the `slot_id` is your soft reservation.

### 3. Collect the user's contact info

`/api/book` needs `customer.name`, `customer.email`, and `customer.phone` — all three are required, no exceptions. The merchant uses these to confirm the appointment, send reminders, and contact the user if something changes. **Do not invent these values, do not reuse a previous user's info, and do not pull them from cached context without asking.**

Ask the user directly:

> To book this appointment I need three things: your full name, an email address (the merchant sends the confirmation here), and a phone number (most platforms reject bookings without it).

If the user refuses to provide any of the three, stop and tell them the booking can't proceed — most platforms reject the request server-side.

### 4. Confirm with the user — explicitly

Once you have the contact info, present a one-sentence summary and ask for explicit "yes":

> Book **<service.name>** at **<business_name>** on **<date>** at **<time>** for **<customer.name>** (`<customer.email>`, `<customer.phone>`)? \$0.25 USDC booking fee (paid now via your wallet). The service itself (<service.estimated_price if known>) is paid directly at the location.

Show all three customer fields back to the user so they can spot a typo before we charge $0.25.

The only money Rate charges is the **$0.25 USDC booking fee**. The slot's `estimated_price` is the merchant's price for the service — paid in person, not by your agent or Rate. Don't conflate the two when summarizing for the user.

**Never call `/api/book` without explicit user confirmation.**

### 5. Hand the booking to your wallet skill

```
POST /api/book
Content-Type: application/json
Idempotency-Key: <stable per-attempt key, e.g. ag_req_4f1c>

{
  "booking_url": "https://...",
  "slot_id": "<slot_id from step 2>",
  "service_name": "Men's Haircut",
  "start_time": "2026-05-03T10:30:00-07:00",
  "customer": { "name": "Ada Lovelace", "email": "ada@example.com", "phone": "+15555550100" },
  "user_confirmation": "yes"
}
```

Don't make the request yourself — invoke the wallet skill's payment primitive on this URL/body so it handles the 402 → sign → retry loop:

- **Awal**: invoke the `pay-for-service` skill on `POST https://www.rateai.dev/api/book` (the skill auto-triggers when it sees a 402; falls back to `npx awal@2.8.2 x402 pay <url> -X POST -d '<json>' --max-amount 250000`)
- **Tempo**: `tempo request POST https://www.rateai.dev/api/book --header "Idempotency-Key: <key>" --body '<json>'`
- **AgentCash**: `npx agentcash fetch https://www.rateai.dev/api/book --method POST --header "Idempotency-Key: <key>" --body '<json>'`

The wallet sees the 402 (with `PAYMENT-REQUIRED` header), signs the EIP-712 USDC authorization, retries with `PAYMENT-SIGNATURE`. After verification, **the server returns HTTP 202 within ~3 seconds** with a poll URL. Same `Idempotency-Key` on every retry so a transient network blip doesn't double-book.

### 6. Read the queued response (HTTP 202) and start polling

The booking is **asynchronous**. Once your wallet's authorization clears, Rate returns 202 immediately while the actual booking + on-chain settlement runs in the background (~80 seconds: spin up a session, drive the booking page through to confirmation, settle the USDC transfer on Base).

> ⚠ **`booking.status: "paid"` in the 202 response does NOT mean USDC moved.** It means: "your EIP-3009 authorization passed off-chain `verify`; the booking is queued for execution." The actual on-chain settlement happens later, after the Browserbase booking succeeds. Rate's pay-on-success guarantee still holds — if the booking ends in `needs_human` or `failed`, no `transferWithAuthorization` ever lands on Base. You can confirm by reading `payment.tx_hash`: it's `null` in the 202 body and stays `null` for unsettled bookings; it gets populated only after the on-chain settle. **Don't tell the user "paid" until you've polled through to `booking.status === "booked"`.**

```json
{
  "booking": {
    "id": "apt_xxxxxxxx",
    "platform": "square",
    "status": "paid",
    "confirmation_text": null,
    ...
  },
  "payment": { "id": "pay_xxxx", "tx_hash": null, "currency": "USDC", ... },
  "receipt": null,
  "trace": { "id": "trc_xxxx", "events": [...] },
  "idempotency_key": "ag_req_4f1c",
  "status": "queued",
  "poll_url": "/api/bookings/apt_xxxxxxxx",
  "next_step": "Poll GET /api/bookings/apt_xxxxxxxx every 3s until booking.status flips from \"paid\" to \"booked\" / \"needs_human\" / \"failed\"."
}
```

**Poll** `GET https://www.rateai.dev/api/bookings/<id>` every ~3 seconds until `booking.status` is no longer `"paid"`:

- `booked` → success. Read `confirmation_text`, `confirmation_data.referenceId`, `confirmation_data.manageUrl`, `payment.tx_hash`. The full `loadFullBooking` shape.
- `needs_human` → the booking page required something Rate can't do (login, CAPTCHA, card, deposit). Tell the user to complete it manually. The authorization expires unsettled — no on-chain charge.
- `failed` → hard error mid-flow. Same: no charge.

The `payment.status` field tracks the on-chain side independently: `authorized` (verified, queued) → `settled` (booking succeeded, `tx_hash` is now real) / `unsettled` (booking failed; authorization expired, never submitted) / `settlement_failed` (rare race: booking succeeded but settle reverted; contact support).

A reasonable polling pattern is **every 3s, max 50 polls (~150s)**. Most bookings finish in 60–120s. If you're still seeing `paid` after 150s, something's wrong on our side — contact support; the trace_id is your reference.

**Push alternative**: if you've registered a webhook (`POST /api/webhooks`), `booking.confirmed` / `booking.failed` / `booking.cancelled` events fire automatically when the booking settles to a terminal state. No polling needed.

### 7. Show the user the confirmation

Once `booking.status === "booked"`, quote `confirmation_text`, `service_name`, `start_time`, `confirmation_data.referenceId`, and `receipt.url` to the user.

### Optional: cancel a booking

```
POST /api/bookings/apt_xxxxxxxx/cancel
```

Free. Returns `{ booking_id, status: "cancelled", cancelled_at, cancellation_text, trace_id }`. Idempotent — calling on an already-cancelled booking returns its current state without re-running the browser.

### Optional: subscribe to events

```
POST /api/webhooks
Content-Type: application/json

{ "url": "https://your-server.example.com/rate-events", "events": ["booking.confirmed", "booking.cancelled"] }
```

Save the returned `secret`. Each delivery includes an `X-Rate-Signature` header — HMAC-SHA256 of the body using your secret.

## Critical rules

- **`user_confirmation: "yes"` is required** in the `/api/book` body. Validated server-side.
- **Confirm before booking.** No exceptions. Show business, service, time, and the $0.25 USDC fee.
- **All three `customer` fields (`name`, `email`, `phone`) are required.** Ask the user — never invent, never reuse a previous user's info, never pull silently from cached context. If the user won't provide all three, abort.
- **`slot_id` expires in 10 minutes.** Re-call `/api/availability` if the user takes longer.
- **Always send `Idempotency-Key`** on `POST /api/book`. Same key on retry. Different keys = double-charge risk.
- **Honor `needs_human`** (422) — Rate cannot continue past login/CAPTCHA/card/deposit. Tell the user.
- **`date` is `YYYY-MM-DD`** in the user's local date — not a timestamp.

## Errors

```json
{
  "error": {
    "code": "<stable_code>",
    "message": "<human readable>",
    "retryable": true,
    "next_step": "<what to do>"
  }
}
```

Codes you must handle:

- `idempotency_required` — add `Idempotency-Key` and retry.
- `confirmation_required` — set `user_confirmation: "yes"` (after explicit user OK).
- `bad_request` — read `next_step`.
- `payment_required` (402) — first contact; body includes the x402 challenge.
- `payment_invalid` (402) — `X-PAYMENT` didn't satisfy the challenge.
- `slot_expired` (410) — call `/api/availability` again.
- `needs_human` (422) — booking page requires human action.
- `browser_failed` (502) — retryable once.
- `not_found` (404) — booking id doesn't exist.
- `internal` (500) — back off, retry once.
