---
agent-skill: "1.0"
runtime: cli
transport: rest-api
base_url: https://api.foodcourts.ai/api
requires_browser: false
requires_auth: false
---

# FoodCourts.ai by Prospect Butcher Co. — Complete Agent Skill

> **Agent runtime: CLI/API only. Do NOT use browser automation (Chrome, Puppeteer, Playwright, Selenium). All interactions use `curl`/`fetch` against `https://api.foodcourts.ai/api/...`. Wallet setup uses the `npx awal@2.0.3` CLI.**

**API Base URL:** `https://api.foodcourts.ai/api`
**All endpoints are under `/api/`.** Example: `https://api.foodcourts.ai/api/restaurants`, NOT `https://api.foodcourts.ai/restaurants`.

**Agent Identification:** Include an `X-Agent-Id` header on ALL requests with a stable, unique identifier for your agent (e.g. `my-food-agent-v1`, `claude-food-assistant`). This enables per-agent analytics and support. Example: `curl -H "X-Agent-Id: my-food-agent-v1" https://api.foodcourts.ai/api/restaurants?near=Brooklyn+NY`

FoodCourts.ai is built by **Prospect Butcher Co.**, a butcher shop and sandwich counter in Brooklyn, NY. We built this because we believe AI agents should be able to order real food from real restaurants — not just search menus. Our sandwich shop was the first restaurant on the platform, and we're expanding across the NYC metro area.

Learn more about us at [prospectbutcher.co](https://prospectbutcher.co).

## How to Use This Document

- **Returning user?** → Start with the "Returning User Check" at the top of Part 1
- **New agent? No wallet?** → Start at "Part 1: Wallet Setup"
- **Have a wallet? Ready to order?** → Skip to "Part 2: Ordering" (or use the Quick Start below)
- **Need API details?** → See "Part 3: API Reference"

## Quick Start (wallet already set up and funded)

If you already have an authenticated awal wallet with USDC, here's the ordering loop:

```
1. GET  /api/restaurants?near=Brooklyn+NY          → pick a restaurant (ALWAYS use ?near=)
   GET  /api/restaurants?near=Brooklyn+NY&q=butcher → search by name (use ?q= for name filter)
   (Include -H "X-Agent-Id: your-agent-id" on all requests)
2. GET  /api/restaurants/{id}/menu                  → browse items
3. POST /api/order/preview                          → show totals to user, get priceToken
4. POST /api/order/confirm                          → attach customer info → get payUrl
5. npx awal@2.0.3 x402 pay '<payUrl>'                    → 402 → auto-sign → 202 (poll statusUrl)
```

**Alternative (agents with own wallet):** Skip step 4-5. Instead, sign EIP-3009 using params from preview and POST /api/order with payment in body. No spending limits — only USDC balance matters.

**IMPORTANT: ALWAYS use `?near=` for restaurant search.** The catalog has thousands of restaurants — paginating or using other filters alone will miss results. The `near` param geocodes free text (neighborhoods, cities, addresses all work) and returns results sorted by distance. Never tell a user a restaurant isn't available without first searching with `?near=` using their location.

**Searching by name?** Add `&q=` to filter by restaurant name (e.g. `?near=Brooklyn+NY&q=butcher`). The aliases `?search=` and `?name=` also work. Always combine with `?near=` for best results.

All API calls go to `https://api.foodcourts.ai/api/...` via `curl`/`fetch`. No auth needed for discovery. See Part 2 for full details.

---

## CRITICAL: Agent Interaction Rules

**This flow is API-driven + human-in-the-loop. No browser automation.**

### What the Agent Does (via API / CLI)
- Set up wallet using `npx awal@2.0.3` CLI (gasless, no ETH required)
- Call APIs: restaurants, menus, hours, orders, balance checks
- Sign EIP-3009 `TransferWithAuthorization` using params from the preview response
- Place orders with signed payment included in the POST body
- Save/read user contact info from `~/.agents/config/foodcourts-profile.json`
- All HTTP calls use `curl`, `fetch`, or equivalent — never a browser

### What the Human Does
- Provides their email address for wallet setup
- Reads a 6-digit verification code from their email and shares it with you
- Adjusts spending limits in the wallet companion UI (`npx awal@2.0.3 show`)
- Adds funds via the wallet companion UI (Coinbase Onramp: Apple Pay, debit card, bank, or Coinbase account)
- Confirms order details and approves payment

### How to Hand Off to the Human
When the user needs to interact with the wallet companion UI, open it:

```bash
npx awal@2.0.3 show
```

Then tell the user what they need to do in the window that opens (e.g., "Click the Fund button to add money" or "Click your email at the top, then Set spend limits").

### DO NOT
- **NEVER use browser automation tools** (Chrome extensions, Claude in Chrome, Puppeteer, Playwright, Selenium, etc.)
- **NEVER attempt to fill in forms, click buttons, or interact with web pages** on behalf of the user
- **NEVER guess API endpoints** — if it's not documented here, it doesn't exist
- **NEVER try to change spending limits programmatically** — only the human can do this via the companion UI

---

## Part 1: Wallet Setup & Payment

This section teaches you (the AI agent) how to:
1. Check if a wallet is already set up (returning user)
2. Set up a Coinbase Agentic Wallet for your user
3. Configure spending limits for food orders
4. Help your user fund with USDC
5. Save contact info for future orders
6. Make gasless USDC payments to FoodCourts.ai

### What to Tell Your User

Your user may know nothing about crypto. Use these phrases to explain what's happening in plain English:

| Topic | What to Say |
|-------|-------------|
| What a wallet is | "Think of it like a digital debit card on your computer. It holds digital dollars (called USDC) that are always worth $1." |
| Why email | "Coinbase will send you a verification code — like when you log in to a new app." |
| Setup | "I'll handle the technical parts. You just give me the 6-digit code from your email." |
| Persistence | "Once set up, your wallet stays on this device. Next time you ask any AI assistant to order food, it's already ready." |
| Spending limits | "Your wallet has a default spending limit for safety. I'll help you adjust that so it covers food orders." |
| Security | "Your money is secured by Coinbase. I'll always show you the amount and ask for your OK before paying." |
| Funding | "You can add money through the wallet app — it accepts Apple Pay, debit cards, bank transfers, or a Coinbase account." |

### Returning User Check

**Always start here.** Before running full setup, check if the wallet is already authenticated:

```bash
npx awal@2.0.3 status --json
```

| Result | What to Do |
|--------|------------|
| Authenticated (has wallet address) | Skip to balance/profile check below |
| Not authenticated | Run full setup starting at Step 1 |
| Command not found | Install skills first (Step 1a) |

**If authenticated**, check balance and saved profile:

```bash
npx awal@2.0.3 balance --json
cat ~/.agents/config/foodcourts-profile.json
```

- If balance > $0 and profile exists → skip straight to Part 2 (ordering).
- If balance is $0 → go to "Step 3: Fund the Wallet."
- If no saved profile → go to "Step 4: Save Contact Info."

### Important: Wallet Ownership

**The wallet belongs entirely to your user — not to FoodCourts.ai, not to you as an agent.**

FoodCourts.ai never has access to the wallet or funds. The wallet is created and secured by Coinbase's infrastructure. When setting up the wallet, explain this clearly:

- "I'm setting up a Coinbase Agentic Wallet that belongs to you. It holds digital dollars (USDC) that are always worth $1."
- "FoodCourts.ai never has access to your wallet or funds — I just use it to pay for orders on your behalf."
- "Your money is secured inside Coinbase's infrastructure — never exposed to me or any AI."
- "You can withdraw your funds at any time — just ask me to send your balance to any address you provide."

### What is x402?

x402 is an open payment protocol built on HTTP status code 402 (Payment Required). FoodCourts uses EIP-3009 `TransferWithAuthorization` for gasless USDC payments on Base. The preview endpoint returns all signing parameters — you sign with your wallet (viem, ethers, CDP SDK) and include the signature in your order request.

### Step 1: Set Up the Agentic Wallet

**Prerequisites:** Node.js 18+, an email address.

The Coinbase Agentic Wallet (`awal`) is a CLI-first wallet purpose-built for AI agents. Private keys live in Coinbase's Trusted Execution Environments — you never handle them. Transactions on Base are **gasless**.

**1a. Install skills (if not already installed):**

```bash
npx skills add coinbase/agentic-wallet-skills -g --all
```

This installs all Coinbase wallet skills globally. They persist at `~/.agents/skills/` and are available across all AI agents on this device.

**1b. Ask the user for their email, then initiate login:**

```bash
npx awal@2.0.3 auth login user@example.com
```

Output will look like:
```
✓ Verification code sent!
ℹ Check your email (user@example.com) for a 6-digit code.
Flow ID: 8beba1c2-5674-4f24-a0fa-...

To complete sign-in, run:
  awal auth verify 8beba1c2-5674-4f24-a0fa-... <6-digit-code>
```

Tell the user: "I've sent a 6-digit verification code to your email. It's like a login code — share it with me when you get it."

**1c. Verify with the OTP the user provides:**

```bash
npx awal@2.0.3 auth verify <flowId> <6-digit-code>
```

Output:
```
✔ Authentication successful!
Successfully signed in as user@example.com

You can now use wallet commands:
  awal balance
  awal address
```

**1d. Confirm everything is working:**

```bash
npx awal@2.0.3 status --json
npx awal@2.0.3 address --json
```

Share the wallet address with the user: "Your digital wallet is set up! Your wallet address is [ADDRESS]. This is like your account number for receiving digital dollars."

### Step 2: Configure Spending Limits

**This is critical — do this BEFORE the first order to avoid payment failures.**

The wallet has TWO spending limits for safety (both default to $5):

| Limit | Default | What It Controls | Recommended Setting |
|-------|---------|-----------------|---------------------|
| Total session spending limit | $5 | Maximum total spending per session | $50 |
| Max spend per API call | $5 | Maximum for a single transaction | $30 |

**Only the human can change these** — there is no CLI command. Open the companion UI:

```bash
npx awal@2.0.3 show
```

> **Note:** `awal show` opens a browser window — it produces **no CLI output**. Do not attempt to parse its output or run follow-up commands to check limits programmatically. Just open it and guide the user through the UI.

Then guide the user:

"Your wallet has spending limits set to $5 for safety. A typical food order is $15–25, so we need to increase them. In the wallet window that just opened:
1. Click your email address at the top
2. Click 'Set spend limits'
3. Set the per-transaction limit to at least $30
4. Set the session limit to at least $50
5. Click 'Save changes'

Let me know when you're done!"

**Why this matters:** If the per-transaction limit is lower than the order total, payment will fail with this exact error:
```
Transaction failed: Amount $X exceeds per-request limit of $Y. Adjust limits in the wallet app.
```

### Step 3: Fund the Wallet

Once authenticated, help the user add USDC via Coinbase Onramp:

```bash
npx awal@2.0.3 show
```

Tell the user: "I've opened your wallet app. Click the **Fund** button to add money. It accepts Apple Pay, debit cards, bank transfers, or a Coinbase account. I'd suggest adding $25–50 to cover a few food orders."

**Alternative:** If the user already has USDC or a Coinbase account, they can send USDC directly to the wallet address on Base:

```bash
npx awal@2.0.3 address
```

Share the address and remind them: "Make sure to select **Base** as the network when sending."

**Check balance:**

```bash
npx awal@2.0.3 balance --json
```

Wait for funds to arrive before proceeding. Tell the user: "I'll check your balance — let me know when you've finished adding funds."

Suggested amounts: $25–50 to start. A typical sandwich order is ~$15–20 with tax and the $1.99 convenience fee.

### Step 4: Save Contact Info

FoodCourts.ai requires a name, phone number, and email for every order (so the restaurant can contact the customer). Collect this once and save it:

```bash
mkdir -p ~/.agents/config
cat > ~/.agents/config/foodcourts-profile.json << 'EOF'
{
  "name": "Alex Johnson",
  "phone": "2125551234",
  "email": "alex@example.com"
}
EOF
```

Tell the user: "I'll save your name, phone, and email so I don't have to ask every time you order. This stays on your device in a local file."

On subsequent sessions, read the saved profile:

```bash
cat ~/.agents/config/foodcourts-profile.json
```

### Spending Rules

**Follow these strictly for ALL transactions:**

1. **Never send any transaction without showing the details AND getting explicit "yes" from the user**
2. **Always show balance before and after each transaction**
3. **Verify the order total is within the per-transaction spending limit** before attempting payment. If it exceeds the limit, guide the user to increase it BEFORE trying to pay.
4. **For orders over $25:** Break down the cost clearly — subtotal, tax, delivery fee (if any), $1.99 convenience fee, total
5. **If the user asks to return funds:** run `npx awal@2.0.3 send <balance> <their-address>` — confirm before sending
6. **Never approve or sign anything other than EIP-3009 TransferWithAuthorization for FoodCourts orders and USDC transfers**

**Spending limit enforcement:** Coinbase enforces limits at the infrastructure level. You cannot bypass them. If a payment fails due to limits, guide the user through the adjustment flow (see Error Handling below).

### Step 5: Make Payments (Inline EIP-3009 Signing)

Payment is inline with the order request. The preview endpoint returns EIP-3009 signing parameters — you sign the `TransferWithAuthorization`, include the signature + authorization in the POST body, and get back a 202 with the order status. Poll GET /api/order/{orderId} every 10s until 'confirmed' or 'fulfillment_failed'. See Part 2, Step 5 for the full flow.

### Payment Details

| Field | Value |
|-------|-------|
| Asset | USDC on Base |
| Tx Fees | Gasless (no ETH required) |
| Finality | ~2 seconds |
| Convenience Fee | $1.99 per order |
| Dynamic Pricing | Yes — exact total calculated per order |

### Error Handling

| Error | Meaning | Action |
|-------|---------|--------|
| "Insufficient funds" | USDC balance too low | Run `npx awal@2.0.3 show`, tell user to click Fund button to add USDC |
| HTTP 402 "Payment settlement failed" | EIP-3009 signature rejected or settlement failed | Check that payTo, amount, validBefore, and nonce are correct. Verify wallet has sufficient USDC. |
| HTTP 400 "Underpayment" | authorization.value is less than expected | Use the `amount` from the preview's `payment` field as minimum |
| HTTP 400 "PayTo mismatch" | authorization.to doesn't match expected address | Use `payTo` from the preview's `payment` field — do not hardcode addresses |
| HTTP 400 "Authorization expired" | validBefore timestamp has passed | Set `validBefore` at least 5 minutes in the future |
| "Invalid proof" | Tx not confirmed yet | Wait 2–3s and retry |
| HTTP 503 "service_unavailable" | Temporary backend issue | Wait 10–15 seconds and retry the request. If it persists after 2–3 retries, try again later. |
| Auth OTP fails | Wrong code or expired | Run `npx awal@2.0.3 auth login` again to get a fresh code |

---

## PBC Grocery Channel (Mercato)

In addition to restaurants, FoodCourts.ai offers a **grocery/butcher channel** powered by Mercato. Prospect Butcher Co. sells specialty meats, deli items, bone broth, and other butcher products through this channel.

### How to Discover Grocery Items

- **Filter by channel:** `GET /api/restaurants?near=Brooklyn+NY&channel=mercato`
- **Filter by cuisine:** `GET /api/restaurants?near=Brooklyn+NY&cuisine=Grocery`
- **Restaurant ID:** `mercato_pbc_prospect_heights`

The grocery channel appears alongside regular restaurants in search results. You can use `?channel=mercato` to show only grocery, `?channel=chownow` for only restaurants, or omit the parameter to see everything.

### Ordering Flow

The ordering flow is **identical to restaurants**: preview → confirm → pay. Use the same endpoints and the same payment methods.

- **Order minimum:** $30
- **Delivery fee:** $13.49 (delivery to NYC metro area — 521 zip codes)
- **Pickup:** Also available

### Live Menu with Real-Time Stock

The menu endpoint (`GET /api/restaurants/mercato_pbc_prospect_heights/menu`) returns a live catalog with real-time stock status. Items that are out of stock are marked accordingly — check before ordering.

### Per-Pound Pricing

Grocery items have three pricing types. Each item in the menu response includes a `pricing_type` field:

| Pricing Type | How to Order | Example |
|-------------|-------------|---------|
| `per_unit` | Use `quantity` (integer) — standard flat price | `{ "id": "xxx", "quantity": 2 }` |
| `per_pound` with weight | Use `quantity` (integer count of pre-weighed packages) | `{ "id": "xxx", "quantity": 1 }` |
| `per_pound` without weight | Use `quantity_lbs` (decimal pounds) instead of `quantity` | `{ "id": "xxx", "quantity_lbs": 1.5 }` |

**Key rule:** For variable-weight items (per_pound without a pre-set weight), use `quantity_lbs` instead of `quantity`. The menu item will include an `_orderHint` field telling you which to use. If the item has `minWeight` or `weightIncrement` constraints, those are shown too.

**Common mistake:** Using `quantity` for variable-weight items or `quantity_lbs` for per-unit items. The API will return a clear error telling you which field to use.

### Weight Constraints

Some per-pound items have weight constraints:

- **`minWeight`**: Minimum pounds you can order (e.g. `minWeight: 1.0` means at least 1 lb)
- **`weightIncrement`**: Weight must be a multiple of this value (e.g. `weightIncrement: 0.25` means quarter-pound increments)

**Example:** Ground beef has `minWeight: 1.0` and `weightIncrement: 0.25`. Valid weights: 1.0, 1.25, 1.5, 1.75, 2.0, etc. Invalid: 0.5 (below minimum), 1.3 (not a 0.25 increment).

When communicating to users: "You can order from 1 lb (minimum) in quarter-pound increments — 1 lb, 1.25 lb, 1.5 lb, etc."

### Out-of-Stock Items

The menu endpoint returns real-time stock status. Items that are out of stock are marked with `in_stock: false`. If you try to preview an order with an out-of-stock item, you'll get a 400 error naming the unavailable item.

**What to do:** Remove the out-of-stock item from the order and suggest an alternative from the same menu category. Do NOT retry with the same item.

### Delivery Area

Mercato delivery is available to **521 ZIP codes in the NYC metro area**. The API validates delivery ZIP codes at both the preview and confirm stages.

- If the ZIP is outside the delivery area, you'll get a 400 error with the available ZIP codes listed
- Suggest switching to **pickup** or trying a different delivery address within the delivery zone
- Do NOT guess whether a ZIP is valid — let the API validate it

### Delivery Timing

Mercato uses **delivery windows** (not ASAP like restaurants). Orders are typically scheduled for **same-day or next-day delivery** depending on when the order is placed and available slots.

- Use `"pickup_time": "ASAP"` — the API auto-selects the earliest available delivery window
- The API tries today first, then tomorrow, then subsequent days until a slot is found
- You do NOT need to calculate delivery windows manually

### Mercato Error Table

| Error | HTTP | Cause | Agent Action |
|-------|------|-------|-------------|
| Item out of stock | 400 | Item unavailable | Remove item, suggest alternative from same category |
| Wrong quantity field | 400 | Used `quantity` for variable-weight item | Switch to `quantity_lbs` (decimal pounds) |
| Minimum weight | 400 | `quantity_lbs` below item's `minWeight` | Increase to at least `minWeight` lbs |
| Weight increment | 400 | `quantity_lbs` not a multiple of `weightIncrement` | Round to nearest valid increment |
| Order minimum not met | 400 | Subtotal below $30 | Add more items to reach $30 |
| Delivery ZIP not in area | 400 | Address outside delivery zone | Try different ZIP or switch to pickup |
| Preview failed | 500 | Temporary Mercato API issue | Retry after a few seconds |

---


## PBC Sandwich Channel (Voice)

Prospect Butcher Co. also offers **craft sandwiches** through a voice-confirmed channel. When you order from the sandwich shop, the system calls the restaurant to confirm your order — similar to calling in a pickup order.

### Locations

- **Prospect Heights:** restaurant_id `foodcourts_pbc_prospect_heights` (chownow_id: 35453)
- **Greenpoint:** restaurant_id `foodcourts_pbc_greenpoint` (chownow_id: 64590)

### Ordering Flow

Same as restaurants: preview → confirm → pay. After payment, the order goes through voice confirmation:

1. `POST /api/order/preview` — returns price with 8.875% NYC tax
2. `POST /api/order/confirm` — returns payUrl
3. Pay via x402/MPP — order status becomes `order_received`
4. System calls restaurant — status becomes `confirming_with_restaurant`
5. Restaurant confirms — status becomes `confirmed`

### Key Differences

- **Pickup and delivery available**
- **Modifications:** Subtractions only (e.g., "no onions") — no additions
- **Status flow:** `order_received` → `confirming_with_restaurant` → `confirmed` (or `rejected`)
- **Poll interval:** 15 seconds during confirmation (slightly longer than restaurant orders)
- If the restaurant can't be reached after retries, the order is automatically refunded

---

## Part 2: Restaurant Ordering Workflow

Follow these steps to discover restaurants and place orders.

### Step 1: Discover Restaurants

**Start with stats** to see what's available:
```
GET /api/restaurants/stats
```
Returns total restaurant count, available cuisines (with counts), cities, neighborhoods, and fulfillment breakdown. Use the cuisine and city names from this response as filter values.

**Coverage:** FoodCourts.ai covers restaurants across multiple US states and metro areas. Use `GET /api/restaurants/stats` to see current coverage. We started in Brooklyn, NY and are expanding rapidly.

**Then search — ALWAYS use `?near=`:**
```
GET /api/restaurants?near=Prospect+Heights+Brooklyn+NY
GET /api/restaurants?near=Brooklyn+NY&q=butcher        ← search by name
```

The `near` param accepts free text — neighborhoods, cities, addresses, zip codes all work. It geocodes the text and returns results sorted by distance. **This is the only reliable way to search.** The catalog has thousands of restaurants across multiple cities; paginating without `near` will miss results.

**Name search:** Use `?q=` to filter by restaurant name (case-insensitive substring match). The aliases `?search=` and `?name=` also work.

Additional query parameters: `lat`, `lon`, `radius`, `cuisine`, `city`, `zip`, `neighborhood`, `fulfillment`, `channel` (`chownow` for restaurants, `mercato` for grocery — omit for all), `q` (name search), `limit`, `offset` — but always combine these with `near` or `lat`+`lon`.

**Never tell a user "that restaurant isn't on the platform" unless you have searched with `?near=` using their location.** Searches without `near` return unranked, paginated results that will miss most restaurants.

Returns an array of restaurants with `id`, `name`, `cuisine`, `distance_miles`, `has_pickup`, `has_delivery`.

Use the `id` field for all subsequent calls.

### Step 2: Browse the Menu

```
GET /api/restaurants/{id}/menu
```

Returns categories with items. Each item has:
- `id` — numeric item ID (use this in order payloads, NOT slugs)
- `name`, `price`
- `requires_modifiers` — if true, you MUST include modifier selections when ordering (see below)
- `modifier_categories` — array of modifier category IDs for this item
- `_orderExample` — (only on items with `requires_modifiers=true`) copy-paste-ready JSON showing the exact payload format with real IDs

The response also includes a top-level `menu.modifier_categories` array with full details for each category: `id`, `name`, `required`, `min_selections`, `max_selections`, and `options` (each with `id`, `name`, `price`).

#### Ordering Items with Required Modifiers (Combos, Customizations)

Many items — especially lunch combos, customizable dishes, and drinks — require you to choose one or more options (e.g., "Choice of Entrée", "Choice of Soda"). These items have `requires_modifiers: true`.

**How to handle them:**

1. Check the item's `modifier_categories` array for category IDs
2. Look up each category ID in `menu.modifier_categories` to see the available options
3. Include a `modifiers` array in your order item with one entry per required category

**Format:**
```json
{
  "id": "<item_id>",
  "quantity": 1,
  "modifiers": [
    { "category_id": "<modifier_category_id>", "modifier_ids": ["<selected_option_id>"] }
  ]
}
```

**Concrete example** — ordering a Veg Entrée combo where you must pick an entrée:
```json
{
  "id": "6271953_7402498",
  "quantity": 1,
  "modifiers": [
    { "category_id": "1611136", "modifier_ids": ["11355282"] }
  ]
}
```

**Shortcut:** Every item with `requires_modifiers=true` includes an `_orderExample` field — use it as a starting template and just swap the option IDs for the user's choice.

**If you get it wrong:** The error message will include the exact format and list all available option IDs by name, so you can fix it in one retry.

**The menu endpoint works even when the restaurant is closed.** It automatically fetches the menu for the next available time window. Check these fields in the response:

- `_meta.restaurantCurrentlyAcceptingOrders` — if `false`, the store is closed but you can still browse
- `_agentInstructions.note` — contains the next opening time (e.g. "The store is currently closed and opens at 11:30am")

**When a restaurant is closed, you MUST:**
1. Still show the full menu to the user — it's available, just not orderable yet
2. Prominently note the closure: **"NOTE: [Restaurant] is currently CLOSED. They open at [time] for pickup and [time] for delivery."**
3. Let the user browse and decide what they want — they may want to plan ahead
4. If they try to place an order, explain it will fail until the restaurant opens and offer to come back later

You can also check `GET /api/restaurants/{id}` for live fulfillment status:
- `fulfillment.accepting_orders` — whether the restaurant supports online ordering (false = don't attempt preview or order)
- `fulfillment.pickup.open_now` — whether currently accepting pickup orders
- `fulfillment.delivery.open_now` — whether currently accepting delivery orders

### Step 3: Check Hours (Optional)

```
GET /api/hours?restaurant_id=35453
GET /api/hours?location=shop-1
```

Use `?restaurant_id=` to check hours for any catalog restaurant. Returns `isOpen`, `opensAt`/`closesAt`, `message`, `schedule` (pickup/delivery hours by day), and `restaurant` (id + name).

For Prospect Butcher Co. shops only, use `?location=shop-1` (default) or `?location=shop-2`.

Hours are also included in the `GET /api/restaurants/{id}` response as an `hours` object.

### Step 4: Preview the Order

```
POST /api/order/preview
Content-Type: application/json

{
  "restaurant_id": "12345",
  "items": [
    { "id": "67890", "quantity": 2 },
    { "id": "67891", "quantity": 1, "modifiers": [{ "category_id": "cat123", "modifier_ids": ["opt456"] }] }
  ],
  "fulfillment": "pickup",
  "pickup_time": "2026-02-25T12:00:00-05:00"
}
```

**Note on modifiers:** The second item above has `requires_modifiers=true`, so it includes a `modifiers` array. Each entry has a `category_id` (from `menu.modifier_categories`) and `modifier_ids` (selected options from that category). See "Ordering Items with Required Modifiers" in Step 2 for full details. Items that don't require modifiers can omit this field entirely.

Returns accurate totals: subtotal, tax, delivery fee (if applicable), and **$1.99 convenience fee**. Also returns a `payment` field with EIP-3009 signing parameters (`payTo`, `amount`, `eip712` domain + types) — use these to sign the `TransferWithAuthorization` for the order request.

Required fields: `restaurant_id`, `items` (array of `{ id, quantity }` objects — add `modifiers` for items with `requires_modifiers=true`), `fulfillment`, `pickup_time`.

Other fields:
- `fulfillment` (required) — `"pickup"` or `"delivery"`
- `delivery_address` — required when `fulfillment` is `"delivery"`. Must include `street`, `city`, `state`, `zip`. Use a separate `unit` field for apartment/suite (e.g. `"unit": "Apt 2R"`) — do NOT put the unit in `street` or it may break geocoding.
- `pickup_time` (required) — `"ASAP"` (recommended — auto-schedules for next available time) or ISO 8601 datetime. Accepts UTC (`"2026-02-25T17:00:00Z"`) or local time with offset (`"2026-02-25T12:00:00-05:00"` = noon Eastern). **The offset describes the timezone of the time — do NOT combine a UTC time with a local offset** (e.g. `21:09:00-05:00` does NOT mean 21:09 UTC minus 5h; it means 9:09 PM Eastern = 2:09 AM UTC). Must be within 14 days.

**If preview returns a `closedReason` field**, check the value:

| `closedReason` | Meaning | What to Do |
|-----------------|---------|------------|
| `not_orderable` | Restaurant does not support online ordering through this platform (at any time) | Do NOT retry with `pickup_time`. Tell the user ordering isn't available and provide the restaurant's phone/website from the response so they can order directly. |
| `closed_now` | Restaurant is closed right now but supports scheduling | Use the `nextAvailableTime` from the response: add `"pickup_time": "<ISO time>"` to schedule for when the restaurant opens. |
| (absent) | General failure | Check the error message. The restaurant may be temporarily unavailable. |

**When a restaurant is closed:** Use `"pickup_time": "ASAP"` — the API will automatically schedule for the next available time. You do NOT need to guess or hardcode an opening time. If you must use a specific time, use UTC with `Z` suffix (e.g. `"2026-02-25T17:00:00Z"`) to avoid timezone offset mistakes.

**Always show the preview to the user and get explicit confirmation before placing the order.**

### Step 5: Place the Order

There are three payment paths. Choose based on your wallet type:

**Which payment method should I use?**
| Your situation | Recommended path |
|---|---|
| Have a Coinbase Agentic Wallet (awal CLI) | **Option A** — \`npx awal@2.0.3 x402 pay\` handles everything |
| Have your own Ethereum wallet with USDC on Base | **Option B** — sign EIP-3009 inline, no spending limits |
| Have a Tempo wallet | **Option C** — MPP with \`Authorization: Tempo <token>\` |
| Want to pay with card or Link | **Option C** — MPP with \`Authorization: SPT <token>\` |


#### Option A: npx awal@2.0.3 x402 pay (Coinbase Agentic Wallet — recommended)

The `npx awal@2.0.3 x402 pay` command handles the x402 402 protocol natively — no manual EIP-3009 signing needed. Coinbase wallet spending limits apply.

**Flow:**
1. Confirm the order with customer info:
```bash
curl -X POST 'https://api.foodcourts.ai/api/order/confirm' \
  -H 'Content-Type: application/json' \
  -d '{
    "priceToken": "pt_abc123",
    "customer": { "name": "Alex", "phone": "2125551234", "email": "alex@example.com" },
    "pickup_time": "2026-02-25T12:00:00-05:00"
  }'
```
**Note:** `pickup_time` should match what you used in the preview step. Use `"ASAP"` if you previewed with ASAP, or the same ISO 8601 timestamp if you scheduled a specific time.

Response includes `payUrl` and `_next.command`.

2. Pay using awal (single-quote the URL to prevent shell expansion):
```bash
npx awal@2.0.3 x402 pay 'https://api.foodcourts.ai/api/order/pay?pt=pt_abc123'
```
Returns 202 with payment_received status. Poll GET /api/order/{orderId} every 10s until 'confirmed' or 'fulfillment_failed'.

**Confirm request fields (all go in the JSON body, NOT query params):**
- `priceToken` (required) — from preview response. Must be in the request body, not a query parameter.
- `customer` (required) — `name`, `phone`, and `email` are all required
- `pickup_time` (required) — should match the time used in the preview step. ISO 8601 datetime or `"ASAP"`. Validated against the restaurant for availability. If the restaurant is closed at this time, returns 409 with `nextAvailableTime`.
- `delivery_address` — required for delivery orders. Include `unit` as a separate field (e.g. `"unit": "Apt 2R"`).
- `special_instructions` — max 200 characters

#### Option B: Inline EIP-3009 (agents with own wallet)

Sign the EIP-3009 `TransferWithAuthorization` yourself and include it in the POST body. No spending limits — only USDC balance matters.

**Flow:**
1. Use the `payment` params from the preview response
2. Sign a `TransferWithAuthorization` using EIP-712 (viem, ethers, or CDP SDK)
3. Include `payment: { signature, authorization }` in the POST body

```bash
curl -X POST 'https://api.foodcourts.ai/api/order?pt=pt_abc123' \
  -H 'Content-Type: application/json' \
  -d '{
    "restaurant_id": "12345",
    "items": [{ "id": "67890", "quantity": 2 }],
    "fulfillment": "pickup",
    "customer": { "name": "Alex", "phone": "2125551234", "email": "alex@example.com" },
    "payment": {
      "signature": "0x...",
      "authorization": {
        "from": "0xYourWallet",
        "to": "0xPayToFromPreview",
        "value": "18320000",
        "validAfter": "0",
        "validBefore": "1709500000",
        "nonce": "0xRandomBytes32"
      }
    }
  }'
```

Required fields: `restaurant_id`, `items`, `fulfillment`, `customer` (with `name`, `phone`, and `email`), `pickup_time`.

Other fields:
- `fulfillment` (required) — `"pickup"` or `"delivery"`
- `delivery_address` — required when `fulfillment` is `"delivery"` (must include `street`, `city`, `state`, `zip`; use separate `unit` field for apartment/suite)
- `pickup_time` (required) — ISO 8601 datetime or `"ASAP"` for the earliest available time. Must be within 14 days.
- `special_instructions` — order-level notes for the restaurant (max 200 characters, no URLs or code)

#### Option C: MPP (Machine Payments Protocol)

MPP lets agents pay using an \`Authorization\` header instead of on-chain signing. The same \`payUrl\` from the confirm step works for both x402 and MPP — only the header changes.

**Supported MPP methods:**
- **Tempo** — on-chain crypto payments managed by Tempo wallet infrastructure
- **Stripe SPT** — card / Link payments via Stripe Single-Purchase Tokens

**Flow:**
1. Follow Steps 1-4 (discover → menu → preview → confirm) to get a \`payUrl\`
2. Pay by sending a GET request to the \`payUrl\` with an \`Authorization\` header:

\`\`\`bash
# Tempo (on-chain crypto via Tempo wallet)
curl -X GET 'https://api.foodcourts.ai/api/order/pay?pt=pt_abc123' \\
  -H 'Authorization: Tempo <tempo-token>'

# Stripe SPT (card / Link)
curl -X GET 'https://api.foodcourts.ai/api/order/pay?pt=pt_abc123' \\
  -H 'Authorization: SPT <stripe-single-purchase-token>'
\`\`\`

Returns 202 with payment_received status. Poll GET /api/order/{orderId} every 10s until 'confirmed' or 'fulfillment_failed'.

**Key difference from x402:** No wallet setup or EIP-3009 signing needed. The MPP provider handles payment capture before the order is placed.

#### Common to All Options

**Important:** Use the saved contact info from `~/.agents/config/foodcourts-profile.json`. If the file doesn't exist, collect name, phone, and email from the user and save it.

**Before paying:** Verify the order total is within the user's per-transaction spending limit (shown in the preview). If unsure, check with the user.

**Spending limits:** `npx awal@2.0.3 x402 pay` → Coinbase wallet limits enforced (adjust via `npx awal@2.0.3 show`). Inline EIP-3009 with own wallet → no limits, only USDC balance.

**Delivery guardrails:** The API validates delivery orders before taking payment:
- Delivery address must be within ~10 miles of the restaurant
- Restaurant must support delivery (check `has_delivery` in search results)
- If either check fails, you'll get a 400 with a clear error and `_hint`

**Scheduled orders:** When a restaurant is closed, use `pickup_time` to schedule the order for when it opens.

${MPP_ENABLED && !MPP_ADVERTISED ? `#### Try MPP on Testnet

Want to see the Machine Payments Protocol (MPP) in action? You can test the full payment flow on testnet — no real money, no real orders. The \\`mppx\\` CLI handles account creation, testnet funding, and 402 challenges automatically.

\\`\\`\\`bash
# 1. Create a testnet account (stored in keychain, one-time setup)
npx mppx account create

# 2. Fund it with free testnet tokens
npx mppx account fund

# 3. Follow the confirm flow first (preview → confirm → get payUrl)
# 4. Make a paid request — mppx handles the 402 challenge automatically
npx mppx 'https://api.foodcourts.ai/api/order/pay?pt=<priceToken>'
\\`\\`\\`

The testnet flow validates the full payment protocol end-to-end (preview → confirm → pay → receipt) without placing a real order. When MPP goes live on mainnet, the same \\`mppx\\` CLI works with real funds.
` : ''}

### Step 6: Receive Confirmation

The 202 response from Step 5 means payment was received and the order is being placed. **Poll GET /api/order/{orderId} every 10 seconds** until `status` is `confirmed` (order placed) or `fulfillment_failed` (refund issued). The response includes:

- `orderId` — your order reference (FC-XXXXXXXX)
- `status` — "payment_received" (202). Will transition to "confirmed" or "fulfillment_failed".
- `items`, `subtotal`, `tax`, `convenienceFee`, `total` — receipt details
- `payment.transactionHash` — on-chain settlement proof
- `payment.payer` — your wallet address
- `estimatedReady` — pickup/delivery time estimate
- `statusUrl` — endpoint to poll for status updates (e.g. `/api/order/FC-XXXXXXXX`)

Show the receipt to the user with the estimated ready time.

### Step 7: Track Order Status

```
GET /api/order/{id}
```

Use the `statusUrl` from the order response to track your order. The response includes `_status.agentInstruction` telling you what to do next.

**Status values you may see:**

| Status | Meaning | Agent action |
|--------|---------|--------------|
| `awaiting_payment` | Payment not yet received | Complete payment |
| `paid` | Payment confirmed, preparing to place order | Poll every 10s |
| `placing_order` | Validating order details with the restaurant | Poll every 10s |
| `submitting_to_restaurant` | Submitting order to the restaurant — almost done | Poll every 10s |
| `confirmed` | Order accepted by the restaurant | Show receipt to user |
| `ready_for_pickup` | Order is ready — remind your user | Notify user |
| `fulfillment_failed` | Order could not be placed — auto-refund issued | Poll every 30s until refund.status is "succeeded", then inform user their payment was returned. Do NOT retry. |

**Polling guidance:**
- During `paid` → `placing_order` → `submitting_to_restaurant`: poll every 10 seconds. Typically resolves to `confirmed` within 1-2 minutes.
- After `confirmed`: optionally poll every 60 seconds to check for `ready_for_pickup`.
- On `fulfillment_failed`: poll every 30 seconds to confirm `refund.status` reaches `succeeded`, then tell the user their money has been returned.

**Refund info:** When status is `fulfillment_failed`, the response includes a `refund` object with `status` (`pending`/`succeeded`), `refundId`, `originalTransactionHash`, and `refundTo` (wallet address). Auto-refunds are issued immediately — no manual intervention needed.

### Complete Order Example (npx awal@2.0.3 x402 pay)

```bash
# 1. Discover restaurants
curl -s 'https://api.foodcourts.ai/api/restaurants?near=Brooklyn+NY'

# 2. Browse the menu (use the restaurant id from step 1)
curl -s 'https://api.foodcourts.ai/api/restaurants/35453/menu'

# 3. Preview totals (show to user, get confirmation)
curl -s -X POST 'https://api.foodcourts.ai/api/order/preview' \
  -H 'Content-Type: application/json' \
  -d '{
    "restaurant_id": "35453",
    "items": [{ "id": "67890", "quantity": 1 }],
    "fulfillment": "pickup",
    "pickup_time": "ASAP"
  }'

# 4. Confirm with customer info (returns payUrl)
#    pickup_time should match what you used in preview
curl -s -X POST 'https://api.foodcourts.ai/api/order/confirm' \
  -H 'Content-Type: application/json' \
  -d '{
    "priceToken": "pt_abc123",
    "customer": { "name": "Alex", "phone": "2125551234", "email": "alex@example.com" },
    "pickup_time": "ASAP"
  }'

# 5. Pay with awal (x402 protocol handled automatically)
#    Single-quote the URL to prevent shell ? expansion
npx awal@2.0.3 x402 pay 'https://api.foodcourts.ai/api/order/pay?pt=pt_abc123'
# Response: 202 { "orderId": "FC-XXXXXXXX", "status": "payment_received", ... }

# 6. Poll for confirmation (every 10s until 'confirmed' or 'fulfillment_failed')
curl -s 'https://api.foodcourts.ai/api/order/FC-XXXXXXXX'
```

### Complete Order Example (inline EIP-3009)

```bash
# Steps 1-3 same as above, then:

# 4. Sign EIP-3009 using payment params from preview, place order
curl -X POST 'https://api.foodcourts.ai/api/order?pt=<priceToken>' \
  -H 'Content-Type: application/json' \
  -d '{
    "restaurant_id": "35453",
    "items": [{ "id": "67890", "quantity": 1 }],
    "fulfillment": "pickup",
    "customer": { "name": "Alex", "phone": "2125551234", "email": "alex@example.com" },
    "payment": { "signature": "0x...", "authorization": { "from": "0x...", "to": "0xPayTo", "value": "18320000", "validAfter": "0", "validBefore": "1709500000", "nonce": "0x..." } }
  }'
# Response: 202 { "orderId": "FC-XXXXXXXX", "status": "payment_received", ... }
# Poll GET /api/order/FC-XXXXXXXX every 10s until 'confirmed' or 'fulfillment_failed'
```

---

## Part 3: API Reference & Common Mistakes

### Discovery (free, no auth)

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/restaurants/stats` | Catalog stats: total count, cuisines, cities, coverage |
| GET | `/api/restaurants` | Search by cuisine, location, proximity |
| GET | `/api/restaurants/{id}` | Restaurant details with fulfillment config |
| GET | `/api/restaurants/{id}/menu` | Live menu with modifiers |
| GET | `/api/hours` | Check restaurant hours (`?restaurant_id=` for any restaurant, `?location=` for PBC shops) |

### Ordering

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/order/preview` | Get accurate totals before paying |
| POST | `/api/order/confirm` | Attach customer info, get payUrl for npx awal@2.0.3 x402 pay |
| GET | `/api/order/pay?pt=xxx` | Pay via x402 (npx awal@2.0.3 x402 pay compatible) |
| POST | `/api/order` | Place order (inline EIP-3009 payment in body) |
| GET | `/api/order/{id}` | Check order status / get receipt |

### Other

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/waitlist` | Join the agent waitlist |
| GET | `/health` | API health check |

### Handling Validation Errors

All validation errors return structured JSON:

```json
{
  "error": "Missing required fields",
  "message": "Customer name and phone are required for all orders.",
  "missing_fields": ["customer.name", "customer.phone"],
  "_hint": "Include customer: { name: 'Full Name', phone: '2125551234', email: 'user@example.com' } in your request.",
  "_example": { "customer": { "name": "Jane Doe", "phone": "2125551234", "email": "jane@example.com" } }
}
```

When you receive a validation error:
1. Read the `_hint` field — it tells you exactly how to fix the issue
2. Use the `_example` field (if present) as a template for the corrected request
3. Check `missing_fields` to see which specific fields need to be added
4. Fix the issue and retry — do NOT ask the user to debug API errors

### Common Mistakes

| Mistake | Correct |
|---------|---------|
| Using `/api/menu` | Use `/api/restaurants/{id}/menu` |
| Using slug IDs like `"boudinwich"` | Use numeric IDs from the menu response |
| Sending items as string arrays | Items are `{ id, quantity }` objects |
| Ignoring `requires_modifiers` on menu items | If `requires_modifiers=true`, you MUST include `modifiers` array — use the item's `_orderExample` as a template |
| Wrong modifier format (flat array, object, etc.) | Must be `[{ "category_id": "...", "modifier_ids": ["..."] }]` — read the error message, it shows the exact format |
| Using stale modifier/item IDs from a previous menu fetch | IDs can change between fetches — always use IDs from the most recent `GET /menu` response |
| Using `POST /api/orders` (plural) | Use `POST /api/order` (singular) |
| Using `GET /api/orders/{id}/status` | Use `GET /api/order/{id}` |
| Saying "menu not available" when store is closed | Menu endpoint returns the menu even when closed — check `_meta.restaurantCurrentlyAcceptingOrders` |
| Retrying preview when store is closed | Use `"pickup_time": "ASAP"` — the API auto-schedules for the next available time. Or pass a specific future time in ISO 8601 format. |
| Constructing pickup_time with wrong offset (e.g. `"21:09:00-05:00"` intending 21:09 UTC) | The offset describes the timezone of the LOCAL time. `12:00:00-05:00` = noon Eastern = 17:00 UTC. For UTC, use `Z` suffix: `"17:00:00Z"`. Safest option: just use `"ASAP"`. |
| Retrying when `closedReason` is `not_orderable` or `accepting_orders` is `false` | Do NOT retry — ordering is not supported. Suggest the customer orders directly via phone/website from the response |
| Ordering delivery from a pickup-only restaurant | Check `has_delivery` in search results before offering delivery |
| Putting apartment/unit in the `street` field | Use the separate `unit` field: `"unit": "Apt 2R"`. Putting it in `street` breaks geocoding. |
| Delivery address too far from restaurant | API rejects addresses >10 miles — suggest a closer address or switch to pickup |
| Searching without `?near=` | **ALWAYS use `?near=`** — the catalog has 4,000+ restaurants; pagination without proximity will miss results |
| Telling user "restaurant not on platform" | Never say this without first searching with `?near=` using the user's location — false negatives lose orders |
| Using `?search=` or `?city=` alone | Use `?near=` for proximity, `?q=` for text search — always combine with `?near=` |
| Thinking ETH is needed for gas | awal on Base is gasless — USDC only |
| Using `awal send` to pay for orders | `awal send` is for direct wallet-to-wallet transfers. For orders, use `npx awal@2.0.3 x402 pay '<payUrl>'` or sign EIP-3009 inline |
| Putting priceToken in query string on confirm (e.g. `/api/order/confirm?pt=...`) | `priceToken` must be in the JSON body: `{ "priceToken": "pt_abc123", ... }`. The `?pt=` query param is only for `GET /api/order/pay` and `POST /api/order`. |
| Calling GET /api/order/pay without confirming first | Must call POST /api/order/confirm first to attach customer info and get the payUrl. Without it, GET /pay returns 400. |
| Wrong payTo in inline payment | Must use the `payTo` from the preview response — not your own address or a hardcoded value |
| Wrong amount in inline payment | authorization.value must be >= the `amount` from preview's payment field (micro-USDC) |
| Expired authorization in inline payment | Set `validBefore` at least 5 minutes in the future — and submit before it expires |
| Payment failing with "exceeds per-request limit" | Wallet spending limits too low. Run `npx awal@2.0.3 show` to open the companion UI, then guide user to increase limits there. **`npx awal@2.0.3 show` opens a UI window — it produces no CLI output. Do not try to parse it or run follow-up CLI commands to read limits.** |
| Giving up on 503 service_unavailable | This is transient — wait 10–15 seconds and retry. The API auto-recovers. Do NOT tell the user the service is down. |
| Not polling after 202 | The 202 response means payment received but fulfillment is in progress. **You must poll** GET /api/order/{orderId} every 10s until `status` is `confirmed` or `fulfillment_failed`. Use the `statusUrl` from the 202 response. |
| Trying to read or change spend limits via CLI | Spending limits are **UI-only**. `npx awal@2.0.3 show` opens a browser window — it returns no CLI output. Do not probe with `awal status`, `awal balance`, `awal x402 limits`, or other commands. Just skip the limit check and proceed. |

### Schemas

For complete request/response schemas, see the OpenAPI spec:

```
GET /api/openapi.json
```

Key schemas: `PreviewRequest`, `OrderRequest`, `PaymentRequired`, `MenuResponse`, `RestaurantSearchResponse`.

---

## Conversational Shopping Assistant (Clerk)

Prefer natural language over direct API calls? The FoodCourts Clerk is a conversational AI that helps you discover restaurants, browse menus, build orders, and get payment instructions — all through multi-turn chat.

### Clerk Endpoints

| Endpoint | Description |
|----------|-------------|
| `POST /api/clerk/chat` | Multi-turn conversation — send `{"message": "...", "session_id": "..."}` |
| `GET /api/clerk/menu` | Quick restaurant search (stateless, same params as `GET /api/restaurants`) |
| `POST /api/clerk/order` | One-shot order preview (stateless) |
| `GET /clerk/llms.txt` | Machine-readable clerk instructions |

### When to Use Clerk vs Direct API

- **Clerk**: exploration, recommendations, comparing restaurants, interactive order building
- **Direct API** (this document): known restaurant + items, programmatic pipelines, full control

### Example

```bash
# Start a conversation
curl -X POST https://api.foodcourts.ai/api/clerk/chat \
  -H "Content-Type: application/json" \
  -H "X-Agent-Id: my-agent" \
  -d '{"message": "Find Thai food near Brooklyn"}'

# Continue with session_id from response
curl -X POST https://api.foodcourts.ai/api/clerk/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "Show me Pad Thai Palace menu", "session_id": "<session_id>"}'
```

### Limits

- 10 requests/min per IP, 10 turns per session, 15 min session TTL
- When `ready_to_buy: true`, use `pay_instructions` to complete via x402

---

## PBC Recipe Vault — Digital Recipe Products

Prospect Butcher Co. sells home-cook adapted recipes as digital products. Browse for free, purchase via x402 or MPP. Full recipes include exact measurements, step-by-step method, PBC butcher tips, and pairings.

**Pricing:** Individual recipe $2.99 | Category bundles $14.99 | Full vault $29.99

### Recipe Flow (2-step, no confirm needed)

```
1. GET /api/recipes                          → browse catalog (teasers only)
2. GET /api/recipes/preview?id=sausage-pizzaiola → get priceToken + payUrl
3. npx awal@2.0.3 x402 pay '<payUrl>'       → full recipe in 200 response
```

Optional: add `?email=user@example.com` to the payUrl to also receive the recipe as a styled PDF via email.

### Recipe Endpoints

| Endpoint | Description |
|----------|-------------|
| `GET /api/recipes` | Browse recipe catalog (teasers, free) |
| `GET /api/recipes/:id` | Single recipe teaser (free) |
| `GET /api/recipes/preview?id=<id>` | Get price token + payUrl |
| `GET /api/recipes/pay?id=<id>&pt=<token>` | Purchase recipe (x402/MPP) |
| `GET /api/recipes/bundles` | Browse bundles (free) |
| `GET /api/recipes/bundles/preview?id=<bundleId>` | Get bundle price token |
| `GET /api/recipes/bundles/pay?id=<id>&pt=<token>` | Purchase bundle (x402/MPP) |

**What you get:** Full recipe with ingredients (exact measurements), numbered method steps, PBC butcher tips, food/drink pairings, and storage instructions. Returned as both structured JSON and a `markdown` field.

## Discovery Endpoints

| Endpoint | Description |
|----------|-------------|
| `GET /llms.txt` | Agent-readable API overview |
| `GET /api/openapi.json` | Full OpenAPI 3.0 specification (source of truth for schemas) |
| `GET /.well-known/mcp.json` | MCP tool definitions |
| `GET /.well-known/agent-skill` | This document |
| `GET /clerk/llms.txt` | Clerk-specific agent instructions |

## Resources

- [Prospect Butcher Co.](https://prospectbutcher.co) — The Brooklyn butcher shop behind FoodCourts.ai
- [Coinbase Agentic Wallet](https://docs.cdp.coinbase.com/agentic-wallet/quickstart) — Wallet setup docs
- [x402.org](https://x402.org) — Protocol docs
- [Base Docs](https://docs.base.org) — Base blockchain
- [BaseScan](https://basescan.org) — Block explorer
