Documentation menu

Protocols (UCP / ACP / MCP)

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 — wired in through one createProtocolHandler call.

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 — 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.

The endpoint map

EndpointAdapterBehavior
GET /.well-known/ucpucp/v0Discovery document: store id/origin, capabilities, endpoint URLs. Advertises checkout_handoff only when config.checkout is wired.
GET /.well-known/ucp/v0/catalogucp/v0Paginated 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/checkoutucp/v0Body { "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/checkoutucp/v0Direct-mount aliases of the two above. Discovery always advertises the /.well-known/ forms.
GET /acp/v0/feedacp/v0Product 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 /mcpmcp/v0Single-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).
  • 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 still record the demand.
  • Leave onEvent unset on the protocol handler when it sits behind the gateway — 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.

PSP delegation — no funds, ever

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. Orders that arrive through these endpoints join the funnel like any other — see Outcomes for how attribution works.