# [Protocols \(UCP / ACP / MCP\)](https://rebilder.com/docs/protocols)

> Spec-versioned adapters that make a gateway-fronted store transactable: UCP discovery, catalog, and checkout handoff; an ACP product feed; and an MCP tool server. All wire in through one `createProtocolHandler` call.

- **Updated:** 2026-08-12
- **Publisher:** Rebilder

## What ships in v0

`@rebilder/protocols` implements the three agentic-commerce protocols as thin, **spec-versioned adapters** over the same source-of-truth wiring the gateway uses:

- **UCP**: discovery (`/.well-known/ucp`), a paginated catalog, and a checkout handoff onto your own PSP rails.
- **ACP**: a product feed of schema.org Product records, byte-identical to the structured data your canonical pages embed.
- **MCP**: a JSON-RPC 2.0 server exposing `search_catalog`, `get_product`, and `get_policies` tools.

The handler is deterministic pure assembly: no network calls, no LLM, no clock on the wire, just exact field copies of what your resolvers return, inside the edge budget (p95 < 50ms compute). We implement these specs; we do not invent our own protocol.

> **Honest spec tracking** The `v0` directories are **our v0 adapters, tracking the external spec surfaces as observed in August 2026** (the MCP adapter pins protocol revision `2025-06-18`; the UCP/ACP wire shapes are our pinned v0 tracking of those young specs). These specs churn quarterly. When a spec moves, a new version directory lands and `v0` keeps serving exactly these shapes; "we track spec changes so you don't" is the product.

## The endpoint map

| Endpoint | Adapter | Behavior |
| --- | --- | --- |
| `GET /.well-known/ucp` | `ucp/v0` | Discovery document: store id/origin, capabilities, endpoint URLs. Advertises `checkout_handoff` only when `config.checkout` is wired. |
| `GET /.well-known/ucp/v0/catalog` | `ucp/v0` | Paginated catalog (`?limit=` 1–250, default 50; `?cursor=` from the previous page). Items are exact field copies: `url`, `title`, `price {amount, currency}` (minor units, verbatim), `availability`. |
| `POST /.well-known/ucp/v0/checkout` | `ucp/v0` | Body `{ "product_url": "…" }` → `{ handoff_url }` onto your own checkout/PSP rails. `403 verification_required` unless the request carries the gateway-stamped verified verdict or you opted out; see the verification gate below. `404` when checkout is unconfigured or your handoff declines the product. |
| `GET /ucp/v0/catalog`, `POST /ucp/v0/checkout` | `ucp/v0` | Direct-mount aliases of the two above. Discovery always advertises the `/.well-known/` forms. |
| `GET /acp/v0/feed` | `acp/v0` | Product feed: schema.org Product records, byte-identical to the canonical page’s structured data. A record whose source data fails validation is dropped, never patched. |
| `POST /mcp` | `mcp/v0` | Single-shot JSON-RPC 2.0: `initialize`, `ping`, `tools/list`, `tools/call` with tools `search_catalog {query}`, `get_product {url}`, `get_policies {}`. Standard JSON-RPC error codes; notifications get `202`; batches are rejected (`-32600`); `GET /mcp` is `405` (no SSE stream in v0). |

Every response carries `X-Rebilder-Protocol: <proto>/v0` and every payload carries `spec_version` (`"v0"`). Anything not in the table returns `null`: the request falls through to your normal serving path, and an adapter throw is contained to `null` too: a protocol bug never breaks your site.

## Wiring through the gateway

The gateway deliberately does **not** depend on `@rebilder/protocols`: a store that only wants the markdown path shouldn’t carry protocol adapters, and spec versions must ship on their own cadence without version-bumping the gateway. You construct the handler and pass it in as the `protocols` hook; `ProtocolSources` is structurally identical to `GatewaySources`, so one wiring object serves both configs:

lib/gateway-config.ts

```
import { handleRequest, type GatewayConfig } from '@rebilder/gateway'
import { createProtocolHandler } from '@rebilder/protocols'

const sources = { product, policies, catalog } // ProtocolSources is structurally
                                               // identical to GatewaySources —
                                               // one wiring object serves both

const config: GatewayConfig = {
  storeId: 'store_123',
  sources,
  protocols: createProtocolHandler({
    storeId: 'store_123',
    sources,
    checkout: { handoffUrl: (productUrl) => merchantCheckoutUrlFor(productUrl) },
    // leave onEvent unset here — the gateway already emits one event per request
  }),
  onEvent: (event) => queue(event),
}
```

- The hook is invoked **only** when a request classifies onto the `protocol` path (see [Classification](/docs/classification)).
- A returned `Response` is served as-is, and the request’s event records `response.path: "protocol"` with measured `render_ms`.
- `null`, an unset hook, or a hook that throws all preserve the exact pass-through behavior, event included. Without the hook, protocol routes pass through and your [events](/docs/events) still record the demand.
- Leave `onEvent` unset on the protocol handler when it sits behind the gateway, because the gateway already emits one event per request; setting both double-counts.

## Standalone mount

No gateway (for example a dedicated protocol origin)? Mount the handler on any web-standard runtime and set `onEvent` yourself: every served protocol response (errors included) emits one `RebilderEventV0` with `response.path: "protocol"` and measured `render_ms`. Emission is fire-and-forget and can never break serving.

worker.ts

```
const protocols = createProtocolHandler({ storeId, sources, checkout, onEvent })
export default { fetch: async (req: Request) => (await protocols(req)) ?? new Response('Not found', { status: 404 }) }
```

## The checkout verification gate

The UCP checkout endpoint requires a **cryptographically verified agent by default** (`checkout.requireVerified`, default `true`). Verification is Web Bot Auth (RFC 9421 Ed25519 message signatures), run by the gateway when you inject a key registry; the gateway stamps its verdict on a cloned request as `x-rebilder-agent-verified: true | false` (client-sent values are always overwritten, so a spoofed verdict cannot survive the gateway):

lib/gateway-config.ts

```
import { handleRequest, type GatewayConfig, type AgentKeyRegistry } from '@rebilder/gateway'

const registry: AgentKeyRegistry = { /* operator-populated — see the honesty note below */ }

const config: GatewayConfig = {
  storeId: 'store_123',
  sources,
  protocols: createProtocolHandler({ storeId: 'store_123', sources, checkout }),
  verification: { keys: registry },   // require?: 'protocol' — the default and only v0 scope
}
```

- `x-rebilder-agent-verified: true` → the normal `{ handoff_url }` response.
- Anything else (header `false`, or absent) → `403` with `{"error": "verification_required"}`, checked before the body is even parsed.
- Read endpoints (discovery, catalog, ACP feed, MCP tools) are **never** gated; only the transaction is.

> **When verification is enforced** The shipped key directory (`KNOWN_AGENT_DIRECTORY`) is **empty by design**: we do not invent production platform keys. Until you populate a registry (offline, from the platforms’ published key directories, never fetched on the hot path), every request verifies `false` and a default-configured checkout returns **403 for everyone**: secure by default, but not useful until you either populate keys or opt out with `checkout: { handoffUrl, requireVerified: false }`. The opt-out restores the open pre-gate checkout, which is deliberately low-stakes, since the response is only a redirect onto your own PSP rails. And the verdict header is a **trusted channel, not a credential**: mount the handler behind the gateway (which overwrites it) or behind an edge that strips it; never expose a header-gating handler directly to the internet and call it verified.

## PSP delegation: no funds, ever

> **Checkout is a redirect handoff, not a wallet** Payment execution delegates to your PSP (Stripe, Adyen, Shop Pay, …). Neither the gateway nor the protocol adapters ever hold, move, or custody funds. The checkout response carries exactly `{spec_version, protocol, store_id, handoff_url}` and nothing else: no payload on any endpoint carries a payment field, and a test scans every endpoint to keep it that way.

The same injected-data rules apply as everywhere else: prices, stock, titles, and policy text are explicit field copies from your source of truth: nothing invented, nothing reworded, and protocol responses expose the same substance as your canonical HTML page. Source data that fails validation is refused, never rounded or patched.

## Version pinning

- Every spec-version directory (`ucp/v0`, `acp/v0`, `mcp/v0`) carries its **own conformance suite**: golden request/response fixtures asserting the exact wire shapes. A version is not shippable until its conformance tests are green.
- **Upgrading a spec version = a new directory** (`ucp/v1`) with green conformance tests plus a deprecation note on the old one. A shipped version is never edited in place; any change that breaks its conformance suite is by definition a new version. Old versions keep serving until merchants migrate off them.
- Per-version import surface: subpath exports `@rebilder/protocols/ucp/v0`, `/acp/v0`, `/mcp/v0`. Deeper imports are forbidden.

The full export list is on the [API reference](/docs/reference#protocols-package). Orders that arrive through these endpoints join the funnel like any other; see [Outcomes](/docs/outcomes) for how attribution works.