# MERCHANTS.md — add x402 payments to your site through this facilitator

You run a website and want to charge agents per-request via x402. You bring
an HTTP server and one merchant address; we (the operator) run the payment
rails — an open facilitator, epoch-mode payment-channel hubs, a faucet, and
an explorer. This doc is written for whoever builds or configures that
server: a human, or an autonomous agent acting on one.

**Testnet only.** MockUSDC has no real-world value. POL on Polygon Amoy is
testnet gas, not real money. This is an MVP proof of concept: channel funds
trust the hub operator within the challenge window (Level-1, operator-trusted
settlement — see the repo README's "Epoch-root settlement (M8)" section for
the full trust-model disclosure). Do not model custody on this system.

**Where this sits today.** The north star is a public facilitator that any
site can plug into with zero human involvement — this wave delivers it.
Bring any address you control as `payTo` and start quoting: there is no
operator action, no allowlist, and no registration step for either scheme.
`payTo` travels in every `/quote` request you send (§2) and the facilitator
stamps every `accepts[]` entry with it — "which address gets paid" never
touches the facilitator's own config, so there is nothing for an operator to
grant you before you can use the rails.

---

## 0. Prerequisites

This doc uses `curl`, `jq`, and `cast` — `cast` isn't needed until §4's claim
step or §6's balance checks, but it's simplest to get everything in place
now. Idempotent (safe to re-run — only installs what's missing), no root
needed beyond what `apt-get` itself asks for:

```bash
for t in curl jq cast; do command -v "$t" >/dev/null 2>&1 && echo "OK $t" || echo "MISSING $t"; done
command -v curl >/dev/null 2>&1 || echo "MISSING curl - install it with your package manager." >&2

if ! command -v jq >/dev/null 2>&1; then
  if command -v apt-get >/dev/null 2>&1; then sudo apt-get update -y && sudo apt-get install -y jq   # sudo may prompt
  elif command -v brew >/dev/null 2>&1; then brew install jq
  else echo "install jq with your package manager and re-run" >&2; fi
fi

# Foundry (cast/forge) - official installer, then make `cast` usable IN
# THIS SAME SHELL (foundryup only wires up FUTURE shells via your profile):
if ! command -v cast >/dev/null 2>&1 && [ ! -x "$HOME/.foundry/bin/cast" ]; then
  curl -L https://foundry.paradigm.xyz | bash
  export PATH="$HOME/.foundry/bin:$PATH"
  [ -f "$HOME/.foundry/env" ] && . "$HOME/.foundry/env"
  foundryup
fi
export PATH="$HOME/.foundry/bin:$PATH"

cast --version && jq --version   # both must succeed before continuing
```

See PAYING.md §0 for the full per-tool rationale (why the `export PATH`
line is load-bearing, the apt/brew fallback chain) if anything above fails.

---

## 1. The deal

**What we operate:** an open facilitator (`/quote`, `/verify`, `/settle`,
`/supported`, `/meta`, `/healthz`, `/readyz`), one or more epoch-mode
payment-channel hubs behind it, a faucet, and an explorer. You don't hardcode
any of these URLs — discover them from the facilitator's own discovery root:

```bash
META=$(curl -sf "$FACILITATOR_URL/meta")
jq '{chainId, network, usdc, router, hubs, urls, assignment}' <<<"$META"
```

`FACILITATOR_URL` alone is the whole onboarding handout — `/meta` resolves
the chain id, the USDC/router addresses, every live hub (`hub`, `engineUrl`,
`live`), the dispenser/explorer/docs URLs, and a passthrough of the committed
address book's live entry (`deployments/<network>.json`'s last entry — read
it directly via `DEPLOYMENTS_FILE` if you'd rather work offline). A
participating merchant site's OWN `/meta` (the field set PAYING.md §1
documents) is still a supported, documented convention if you'd rather copy
an existing site's shape — but it is no longer the anchor; the facilitator's
`/meta` is.

**What you build:** one HTTP server with a paid route (or several) that
implements the fail-closed decision tree in §3, using an address you control
as `payTo`. You never touch a private key for the common case — see §4.

**What agents do:** the other side of this transaction is
[`PAYING.md`](PAYING.md) — an agent that discovers your route's 402, signs a
payment, and retries with `X-PAYMENT`. Read it once if you want to know
exactly what your callers will send you.

**Tools:** this doc uses `curl`, `jq`, and `cast` (from Foundry) — don't
assume any of them are installed; §0 below checks for each and installs
whatever's missing.

**The two schemes, from your seat:**

| Scheme | How you get paid | Cost to you | Needs registration? |
|---|---|---|---|
| `exact` | One on-chain `transferWithAuthorization` straight to your address, per payment, broadcast by the facilitator's own submitter key | Nothing (facilitator pays gas, out of its own rate-limited daily reservation budget — see §3's `gas-budget-exhausted` row) | No — any address |
| `batch-settlement` (channel) | Accrues off-chain in a hub's ledger; you cash out later with one Merkle-proof claim per hub (§4) | Nothing (any gas wallet, even a stranger's, may submit the claim) | No — any address |

---

## 2. The payment flow: quote-cache → settle → serve

This replicates the amended `crates/site/src/middleware.rs` decision tree
(quote-cache → settle → serve) — read it for the canonical Rust version once
it lands; the pattern below is the same tree in curl/pseudocode. There is no
`payTo`-rewrite step anymore: you put your OWN `payTo` directly into the
`/quote` request, and every `accepts[]` entry comes back already stamped
with it.

**(a) Build — and cache — your 402 body once per resource, not once per payment.**

```bash
QUOTE=$(curl -sf -X POST "$FACILITATOR_URL/quote" -H 'content-type: application/json' \
        -d '{"resource":"https://your.site/api/paid/thing","price":"1000","payTo":"'"$MY_MERCHANT"'"}')
# -> 402-shaped body; every accepts[] entry (exact AND channel alike)
#    already carries payTo == $MY_MERCHANT — nothing left to rewrite
```

Missing or unparsable `payTo` fails loudly, at request time:
`400 {"error":"payTo is required"}` — there is no facilitator-side default
to silently fall back to, so the old "the payTo rewrite is the one thing
everyone gets wrong, and getting it wrong is silent" failure mode is
structurally impossible now: there is nothing left for you to forget to
rewrite.

Serve this body as your 402, **with explicit caching headers** — it is safe
to cache, or even hardcode, per resource, but never blindly forever:

```
Cache-Control: max-age=3600        # your call; recommended <= 24h
ETag: "<hash of the accepts[] content you're serving>"
```

Recommended cached-manifest shape (your own storage/CDN concern, not a
facilitator-side field): carry your own `version` (bump it whenever YOUR
config changes — price or `payTo` rotation) and `expiresAt` alongside the
`accepts` body, so a stored copy self-invalidates. Revalidate — call
`/quote` again — on **all four** of:

1. `expiresAt` lapse;
2. your own config change (price/payTo rotation — merchant-initiated, so
   bump `version` and purge your cache/CDN in the same act);
3. an `unsupported-requirements` decline at settle time (hub-truth rotated
   on redeploy — §3);
4. a changed `hubs`/`deployments` entry in the facilitator's `/meta`
   (topology changed).

No signing: cryptographically signed 402 manifests are a documented roadmap
item (see the README's Roadmap section), not built this wave — the
versioning discipline above is the whole MVP threat-model answer to a stale
cache.

Everything in a channel entry besides `payTo` is **hub-truth** — leave it
byte-for-byte: `extra.contract` (the hub address), `extra.operatorPubKey`,
`extra.receiverAuthorizer`, `extra.withdrawDelay`, and whatever else a hub
adds. A **generic** (payerless) quote's channel entry OMITS
`extra["thunderpolt-hub/v1"].epoch` entirely — epoch is agent-local truth
now (PAYING.md §6), never something you serve from a cache. `resource` must
be your route's absolute public URL, identical every time (pin one public
base URL in your config — never derive it from a request header an attacker
could spoof). `price` is a base-unit decimal string.

**(b) `X-PAYMENT` present — decode, pick the matching cached entry, settle, serve. No re-quote, no verify, on the steady-state path.**

```bash
PAYLOAD=$(base64 -d <<<"$X_PAYMENT_HEADER_VALUE")
SCHEME=$(jq -r .scheme <<<"$PAYLOAD"); PHUB=$(jq -r '.payload.hub // empty' <<<"$PAYLOAD")
# $CACHED_402 is the body you stored/served in (a) for THIS resource:
ENTRY=$(jq -c --arg s "$SCHEME" --arg h "$PHUB" \
  '[.accepts[] | select(.scheme==$s)
    | select($s=="exact" or ((.extra.contract|ascii_downcase)==($h|ascii_downcase)))][0]' <<<"$CACHED_402")
REQ=$(jq -nc --argjson pl "$PAYLOAD" --argjson e "$ENTRY" \
      '{x402Version:2,paymentPayload:$pl,paymentRequirements:$e,method:"GET"}')
S=$(curl -sf -X POST "$FACILITATOR_URL/settle" -H 'content-type: application/json' -d "$REQ")
# success:true -> serve the content, set X-PAYMENT-RESPONSE (envelope below); else map per §3
```

Neither the channel hub's atomic apply nor the exact-scheme settle needs a
`/verify` pre-flight to be safe: both re-validate everything (signature,
epoch, monotonicity, deposit cap, `minDelta`, `payTo`) atomically on the
settle path itself, so a pre-flight buys you nothing on the success path.
`/verify` remains fully supported as an **optional** pre-flight — call it if
you want a free validity check before doing anything else — but it is no
longer part of the documented default flow. Steady state is **one
facilitator call per payment**: `/settle`.

`X-PAYMENT-RESPONSE` envelope (unchanged, the reference site's frozen shape,
base64 of):
```json
{"success":true,"transaction":"<settle.txHash, exact only>","network":"<settle.networkId>",
 "payer":"<settle.payer>","scheme":"<scheme>","replay":"<settle.replay>",
 "receipt":"<settle.receipt, channel only>"}
```

`receipt` (`{seq, payer, merchant, epoch, cumulative, delta, paymentId,
resource}`, that key order) is built by the facilitator from the item it
streamed to the hub plus the hub's per-item ack — the wire shape is
byte-identical to what merchants saw before the hub link went binary
(wave 3); nothing on your side changes.

**(c) x402 v2 header aliases + the resource manifest (recommended, both
optional).** Newer x402 clients speak `PAYMENT-SIGNATURE` / `PAYMENT-REQUIRED`
/ `PAYMENT-RESPONSE`; older ones the `X-` names. The reference site accepts
`PAYMENT-SIGNATURE` as an exact alias of `X-PAYMENT` (read whichever is
present), puts the 402 body base64-encoded in a `PAYMENT-REQUIRED` header on
every 402, and sets BOTH `X-PAYMENT-RESPONSE` and `PAYMENT-RESPONSE` (same
value) on a paid 200 — copying that costs three lines and makes you
interoperable with both generations. Separately, publish your cached 402s up
front at `GET /.well-known/x402` so an agent can pay on its FIRST request to
any of your resources (one RTT saved per new resource):

```json
{"x402Version":2,"generatedAtMs":…,"network":"eip155:80002","payTo":"0x<you>",
 "facilitator":"<FACILITATOR_URL you advertise>","paymentFlow":"upfront",
 "resources":[{"resource":"https://your.site/api/paid/thing","method":"GET","price":"1000",
               "accepts":[ …exactly the accepts[] your 402 for that resource serves… ],
               "extra":{"paymentFlow":"upfront"}}, …]}
```

Rules the reference site follows (`crates/site/src/manifest.rs`): the
entries ARE your cached 402's entries (same `/quote`, same cache — never a
second source of truth); annotate every entry with
`extra.paymentFlow:"upfront"` and channel entries with
`extra.settlementModel:"hub-epoch"` (`docs/x402/
scheme_batch_settlement_thunderpolt_evm.md`; `extra` is not fingerprinted,
so echoing an annotated entry back to `/settle` is safe); serve it with a
content `ETag` + `Cache-Control: public, max-age=<your quote TTL>` and
answer `If-None-Match` with 304; if a hub is down and your `/quote` came back
with fewer channel entries than `/meta` lists hubs, serve the partial
manifest with `Cache-Control: no-store` and do NOT cache it (the same rule
as (a)); if the facilitator is unreachable answer 503, never a stale or
invented manifest. `paymentFlow: upfront` is a promise your middleware
already keeps if it only branches on header presence (as (b) does) — a
request that arrives WITH a payment is settled and served, no prior 402
required. Stale prices resolve one-directionally at the hub (a payer who
signed a lower, old price is declined `unauthorized-cumulative` and re-reads
your fresh 402; one who signed higher is charged the current price) — never
add a second price check after `success:true`.

**Bootstrap/resync only — the one case you'd still call `/quote` per
request.** If the incoming request carries a `PAYMENT-PAYER` header (and,
once that caller has a channel, its `hub`), you MAY forward both as
`payer`/`hub` in a fresh `/quote` call to hand that caller a personalized
entry carrying its current `epoch`. This helps a caller bootstrapping a new
channel, or resyncing after a `stale-epoch` decline (PAYING.md §6/§8) — it
is payer-initiated (triggered by the caller sending `PAYMENT-PAYER`), never
something you do for every payment, and never required for a caller that
already tracks its own `(epoch, cumulative)` locally.

**Doc status.** This section describes the amended-spec behavior
(quote-with-payTo, settle-only hot path, static cacheable 402s) that this
wave's implementation lanes landed. The wire shapes above are code-verified
against the landed binary — `/quote`'s missing/unparsable-`payTo` 400 matches
`crates/facilitator/src/quote.rs`'s `PAY_TO_REQUIRED` constant
(`"payTo is required"`) verbatim, and the `/settle` request's
`x402Version`/`paymentPayload`/`paymentRequirements`/`method` fields match
`crates/facilitator/src/wire.rs`'s `FacilitatorRequest`
(`#[serde(rename_all = "camelCase")]`) field-for-field — but this walkthrough
is **illustrative pending X6's live fresh-agent drill**: no one has yet run
it end to end, unaided, against a running stack. An earlier revision of this
file carried a live-verified walkthrough of the OLD quote→rewrite→verify→
settle flow; that flow is gone. Replace this paragraph with a live-verified
account once X6 runs, or fix the doc if the drill finds a gap.

### Merchant SDK (docs only this wave)

A documented future deliverable — interface sketch only; no code, no npm
package, this wave:

```ts
// @thunderpolt/x402-merchant — future deliverable, interface subject to change
import { paidRoute } from "@thunderpolt/x402-merchant";

app.get("/api/paid/thing", paidRoute({
  facilitatorUrl: process.env.FACILITATOR_URL,   // discovery: /meta or deployments file
  payTo: "0xMERCHANT",                            // any address you control — no registration
  price: "1000",                                  // base units
  resource: "https://shop.example/api/paid/thing",// pinned absolute URL, never derived
  // optional: static402 (pre-baked 402 body, see (a) above), verifyPreflight (default false)
}, handler));
```

Documented semantics are exactly the decision tree above: cached/hardcoded
quote-with-payTo 402 → forward `X-PAYMENT` + the matching cached entry to
`/settle` → fail-closed status mapping per §3 (including the
`rate-limited`/`gas-budget-exhausted`/`merchant-cap-exceeded`/`overloaded`
rows and the settle-side 409) → serve + `X-PAYMENT-RESPONSE`. Express
middleware first; a Workers-compatible (fetch-based, no Node APIs) variant
is roadmap (README's Roadmap section) — combined with static 402s it makes
an edge-native paywall possible: the 402 body is served straight from a CDN
and the only origin-bound work per payment is the single `/settle`
passthrough. Until the SDK ships, §6's reference implementation remains the
normative behavior spec.

---

## 3. Fail-closed rules

The invariant (verbatim from `middleware.rs`'s header comment): **no content
before a successful settle, and settle success already implies the price was
covered** — never run a second price check after `success:true`. What each
facilitator response maps to:

| Facilitator response | You return | Notes |
|---|---|---|
| Any stage (`/settle`, or an optional `/verify`) is unreachable, times out, or answers something you can't parse | **503**, no content | Fail-closed, not a failed payment. Retry the SAME `X-PAYMENT` (same payment-id) later — it converges to the original receipt once you're back; never invent a fresh decline. |
| `/settle` (or `/verify`, if you called it) answers HTTP 400 | **400** | A malformed request — a bug in your integration or the caller's, not a payment decline. |
| `/settle` 200 `success:false` | **402** with `errorReason` (+`hubReason`) | Ordinary declines land here: `insufficient-channel-balance`, `duplicate-payment`, `stale-epoch`, `payment-identifier-conflict`, etc. (channel) or `insufficient-payer-balance`, `authorization-expired`, etc. (exact) — see PAYING.md §8 for the full reason vocabulary from the payer's side. **Most id-conflicts surface HERE now**, at `/settle` — a merchant who skips the optional `/verify` preflight sees a reused-payment-id-with-a-different-voucher conflict as a settle-time `402`/`409` (§7.4's idempotency contract: same payment-id + same requirements fingerprint + same voucher digest replays; a different digest conflicts), not at `/verify`. |
| `/settle` HTTP **409** | **409** with `errorReason` | `payment-identifier-conflict`: the same payment-id was reused with a DIFFERENT voucher — a bug on the paying side, terminal. The old concurrent-duplicate race (`payment-in-flight`) is now joined rather than refused: the facilitator keeps re-sending an in-flight duplicate to the hub until it answers with the original receipt, so a concurrent identical retry settles 200 `replay:true` — you only see `payment-in-flight` if that join outlives the facilitator's settle deadline (~5 s). |
| `/settle` HTTP **429**, `errorReason:"rate-limited"` | **429**, pass through `Retry-After` if present | Exact-scheme only: you're over the facilitator's per-payer or per-IP rate limit on exact settles. Back off; the channel path is unaffected. |
| `/settle` HTTP **429**, `errorReason:"overloaded"`, `Retry-After: 1` | **429**, pass through `Retry-After: 1` | Every one of the facilitator's streams to that hub has its in-flight window full. Nothing was sent, applied or enqueued — the identical retry (same `X-PAYMENT`, same payment-id) is idempotency-safe. Not a payment decline. |
| `/settle` HTTP **503**, `errorReason:"gas-budget-exhausted"` | **503** | Exact-scheme only: the facilitator's own reservation-based daily gas budget is exhausted for the day (§1's cost note). `/verify` still answers honestly; the channel scheme is entirely unaffected — if this payer has a channel, prefer it. |
| `/settle` 200 `success:false`, `hubReason:"payer distinct-merchant cap reached"` (`errorReason:"merchant-cap-exceeded"`) | **402** | Channel-scheme abuse guard: this payer has already paid the hub's configured cap of distinct merchants (default 256) within its current channel epoch. Vanishingly rare for a real payer paying real merchants; falls back to `exact`. |
| `/settle` 200 `success:true` | **200**, MUST serve now | Money moved (channel: hub ledger; exact: on-chain). A retry (same payment-id, same `X-PAYMENT`) replays this exact receipt — never re-decline, never re-charge. |

**Health/readiness monitoring:** `GET $FACILITATOR_URL/healthz` keeps its
existing shape — **200** `{"status":"ok","chainId":…,
"hubs":[{"hub":…,"engineUrl":…,"live":true|false}],"pendingSettles":…}` when
its RPC is reachable, **503** `{"status":"chain-unreachable","error":…}`
when it is not. A new `GET $FACILITATOR_URL/readyz` answers plain,
LB-usable status codes instead: **200** when the instance can serve
meaningful payment traffic; **503** when it can serve NEITHER scheme, or is
booting/draining. An exhausted gas budget is **200-degraded**, not 503 —
`{"ready":true,"exact":"gas-budget-exhausted","channel":"ok"}`-shaped —
because ejecting the whole instance would remove healthy, zero-gas channel
capacity along with the exhausted exact path. Use `/readyz` for load-balancer
health checks; keep using `/healthz` for humans and watchdogs. A hub with
`live:false` in either surface has just silently dropped out of `/quote`'s
channel entries (exact-only degrade for callers of that hub) until it's
live again.

---

## 4. Getting your money

**Exact:** nothing to do. Each successful settle is a real
`transferWithAuthorization` straight to your address — confirm with
`cast call $USDC 'balanceOf(address)(uint256)' $ME --rpc-url $RPC_URL`.

**Channel:** accruals sit in each hub's ledger until you claim them. Claims
are **permissionless** — `claimEpoch`/`claimEpochMulti` always pay the leaf's
named merchant regardless of who submits the transaction, so this step needs
*some* funded EOA's key (yours, a friend's, a small unattended bot's — never
your merchant key, and see the hosted alternative below if you'd rather not
run this loop yourself). Per hub:

```bash
# addresses from /meta or deployments/amoy.json's last entry:
LATEST=$(cast call "$HUB" 'latestEpochId()(uint64)' --rpc-url "$RPC_URL")
DELAY=$(cast call "$HUB" 'epochRootDelay()(uint256)' --rpc-url "$RPC_URL")
# pick the newest id with epochRootPostedAt(id)+DELAY <= now (scan LATEST downward)
P=$(curl -sf "$ENGINE_URL/epoch/proof?merchant=$ME&epoch=$ID")     # 404 -> nothing accrued this hub/epoch
CUM=$(jq -r .cumulative <<<"$P")
[ "$(jq -r .root <<<"$P")" = "$(cast call "$HUB" 'epochRoot(uint64)(bytes32)' "$ID" --rpc-url "$RPC_URL")" ] || exit 1  # never submit on mismatch
ALREADY=$(cast call "$HUB" 'epochClaimed(address)(uint256)' "$ME" --rpc-url "$RPC_URL")   # cumulative AMOUNT cursor, not an epoch counter
PROOF="[$(jq -r '.proof | join(",")' <<<"$P")]"
cast send "$HUB" 'claimEpoch(uint64,address,uint256,bytes32[])' "$ID" "$ME" "$CUM" "$PROOF" \
     --private-key "$(cat .wallets/gas.key)" --rpc-url "$RPC_URL"   # any gas wallet; payout goes to $ME regardless
```

Live-verified end to end on this stack (unaffected by this wave — the claim
mechanics themselves do not change): fetched `/epoch/proof`, confirmed the
engine's `root` matched the hub's on-chain `epochRoot(id)`, submitted
`claimEpoch` from a throwaway wallet that had never touched the merchant key,
and `balanceOf($ME)` increased by exactly the claimed `cumulative` (no more,
no less — the delta equals the channel accrual, distinct from any `exact`
proceeds already sitting in the same balance).

Etiquette and economics: gas per claim is **payer-count-independent**
(~87–101k per hub per cash-out, whether one payer or a million contributed to
that cumulative); claim on your own cadence (hourly/daily is plenty); the
delta must be strictly positive (`CUM > ALREADY`) or the transaction reverts
— that's normal if nothing new accrued, not a bug. Multiple hubs can be
claimed in one transaction via `ClaimRouter.claimEpochMulti(address merchant,
(address hub, uint64 epochId, uint256 cumulativeReceived, bytes32[] proof)[])`
— **all-or-nothing**, so precheck every hub leg the same way as above before
submitting. A lost claim race against a third party claiming first just
reverts that one attempt; the cumulative cursor (`epochClaimed`) means your
next attempt converges automatically — never treat a revert here as data
loss. Reference implementation: `crates/site/src/claim.rs`'s six-step
precheck (engine reachable → hub has roots → newest matured epoch → proof
exists → engine root matches chain → cumulative ahead of `epochClaimed`) is
exactly the logic above, generalized to loop over every hub and batch
through the router automatically.

**Hosted auto-claim (opt-in, no key of your own needed).** If you'd rather
not run the loop above yourself, opt in to the hosted claim-runner:

```bash
curl -s -X POST "$CLAIM_RUNNER_URL/optin" -H 'content-type: application/json' \
     -d '{"merchant":"'"$ME"'"}'
```

Open, no approval step, no identity check. The runner submits
`claimEpoch`/`claimEpochMulti` on a cadence with ITS OWN gas, and the payout
still lands on your address regardless of who submitted the claim (claims
are permissionless, exactly as above) — the worst an abusive registration
can cost the runner is a skipped precheck query, never gas, because the
runner will not submit a claim below its `--min-claim` threshold: a merchant
whose claimable delta is too small to be worth the gas is simply skipped
until it accrues more (nothing is lost by waiting — the cumulative
`epochClaimed` cursor keeps converging on the next funded cycle). This is a
convenience alternative, not a replacement for the DIY path above — both
read the same on-chain roots, so opting in never blocks you from claiming
yourself too.

**Doc status.** Code-verified against the landed `claim-runner` binary: the
`POST /optin` body/path above matches `crates/claim-runner/src/main.rs`'s
hand-parsed `{"merchant": "0x…"}` handler exactly (200 `"registered"` or
200 `"already registered"` for a repeat; 429 with `Retry-After: 1` once the
per-IP rate limit trips; 403 once `--optin-cap` is reached), and the
`--min-claim` dust-filter framing matches its doc comment ("an abusive
registration cannot force a below-threshold claim — it can only cost a
skipped precheck query"). Illustrative pending X6's live fresh-agent drill —
no one has yet exercised this subsection end to end, unaided, against a
running stack.

---

## 5. What the reference site does (that you'd replicate)

`crates/site` is a working example of everything above. Checklist, mapped to
its source:

- Fail-closed middleware order exactly as §2/§3 describe
  (`crates/site/src/middleware.rs`) — one configured `public_base_url`
  feeding every `resource`, never a request header; settle-only on the hot
  path, `/verify` available but not called by default.
- A `/meta` discovery endpoint for ITS OWN callers (recommended for you too —
  copy the frozen field set from PAYING.md §1 if you want agents to be able
  to onboard against your site with zero prior knowledge; the facilitator's
  own `/meta`, §1 above, is the canonical discovery root either way).
- `X-PAYMENT-RESPONSE` on every successful response, plus the optional
  `x-tp-timing-{total,quote,verify,settle,apply,site}-ms` stage headers (nice
  for your own debugging; not required by the spec — the `quote`/`verify`
  fields are present-but-null/omitted once a request skips those stages).
- The x402 v2 header aliases (`PAYMENT-SIGNATURE` in, `PAYMENT-REQUIRED` on
  402s, `PAYMENT-RESPONSE` alongside `X-PAYMENT-RESPONSE`) and the
  `GET /.well-known/x402` resource manifest with `paymentFlow: upfront` —
  §2(c); `crates/site/src/manifest.rs`, `crates/site/src/middleware.rs`.
- Per-request work kept to the minimum on the paid path: the resource URL
  and the EIP-55 `payTo` string are computed once at boot per route, the
  cached 402's `accepts[]` entries are stored pre-serialized and picked by a
  `(scheme, hub)` map lookup, the `/settle` body is spliced from the raw
  `X-PAYMENT` bytes + that entry (the payload is never re-typed), and the
  settle answer is peeked for its frozen fields with the `receipt` passed
  through verbatim — a merchant SDK should do the same (`fac.rs`,
  `middleware.rs`).
- `/healthz` on the site itself is always HTTP 200 with a body `status`
  field — unlike the facilitator (§3), the site never 503s its own
  health check; it degrades quietly instead (a stuck auto-claim cycle shows
  up in the `claim` sub-object, not in `status`).
- An auto-claim cycle exactly like §4, on a timer (`--claim-interval-secs`;
  `0` disables it) — `crates/site/src/claim.rs`, calling the same extracted
  claim library the hosted claim-runner uses.
- The merchant address is COLD everywhere in its own config: it takes an
  address, never a key. The key that actually signs on-chain (auto-claim
  submissions) is a separate, unrelated "site-submitter" key.

---

## 6. Test your integration

1. **Local stack:** `just sp7-stack` (background it; it prints a handout with
   `SITE_URL`/`RPC_URL`/`FACILITATOR_URL`/`DISPENSER_URL`/`DEPLOYMENTS_FILE`/
   `ENGINE_URLS`). There is no registration step — bring any `payTo` and go.
2. **Buy from yourself.** Stand up your paid endpoint, then follow
   PAYING.md §4 (exact) and §6 (channel) against it, using `RESOURCE` set to
   your own endpoint's URL. Confirm the 402's `accepts[].payTo` is your
   address (it is, by construction — you put it in the `/quote` request
   yourself) with the channel entries' `extra` intact, and read
   `cast call $USDC 'balanceOf(address)(uint256)' $MY_MERCHANT` yourself
   BEFORE and AFTER the exact purchase and confirm the delta equals the
   price; `success:true` alone does not prove the money reached you. Confirm
   the channel purchase and its replay both behave per §3 too.
3. **Exact-path spec interop (optional, has a real caveat):** the official
   x402 npm SDK (`clients/x402-smoke`, `just test-x402-interop` idiom) can
   drive a purchase against your endpoint, but its `PaymentRequirementsSchema`
   hardcodes a **fixed enum of ~17 production network names** with no CAIP-2
   support at all (verified by reading the installed SDK's own schema, not
   assumed) — our wire format is always `eip155:<chainId>`, which matches no
   enum member on any chain. Two cases:
   - **Real Polygon Amoy (chain id 80002):** the SDK's `polygon-amoy` name
     already maps to chain id 80002 — the SAME chain — so you only need to
     translate the `network` *label* between `eip155:80002` and
     `"polygon-amoy"` at your server, both directions (on the 402 you serve
     the SDK, and on the signed payload it echoes back to you before you
     forward it to `/settle`). No chain-id workaround needed.
   - **A local/private chain id with no SDK-known name** (this includes
     `just sp7-stack`'s chain id 31337): the SDK cannot represent it at all,
     translation-only isn't enough, and the practical answer is to run your
     own scratch deployment on a chain id the SDK does know a name for
     (`clients/x402-smoke/README.md`'s documented approach: base-sepolia's
     real chain id, 84532) purely for this test.
   `clients/x402-smoke/stub-server.mjs` is a complete, working reference for
   this translation shim — copy its `toSdkAccepts`/`toFacilitatorNetwork`
   pair in front of your own resource server rather than reinventing it.
   This is genuinely optional: channel-scheme SDK interop was never a goal,
   and your own curl-based tests in step 2 already exercise the real
   cryptography end to end.
4. **Fail-closed drill:** stop the facilitator process, confirm your route
   answers 503 with no content for a fresh payment attempt, restart it, and
   confirm the SAME `X-PAYMENT` now converges to 200.

A complete, working answer key for steps 1–2 (~115 lines of python3 stdlib,
implementing an earlier revision of §2's flow) was built at
`.superpowers/sdd/sp7-merchant-refimpl.py` in an earlier wave — that path is
gitignored (`.superpowers/` is a local, uncommitted scratch area) and it
predates this wave's quote-with-payTo/settle-only rewrite, so treat it as a
starting point to update against §2 above, not a drop-in reference anymore.
