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, andget_policiestools.
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
| 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:
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
protocolpath (see Classification). - A returned
Responseis served as-is, and the request’s event recordsresponse.path: "protocol"with measuredrender_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
onEventunset 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.
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):
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 — →403with{"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.