← Blog
August 3, 2026 · 15 min

UCP: the checkout protocol that left payment open

Every agentic commerce protocol we have taken apart so far decided how the money moves. The Universal Commerce Protocol deliberately did not — and that omission is the most interesting thing about it.

This is the fourth agentic commerce specification we dissect in a month. OpenAI and Stripe's ACP bundles checkout and payment into a single delegated token. Visa's Trusted Agent Protocol signs who is knocking at the merchant edge. Mastercard and Google's Verifiable Intent signs what the human approved. All three ship with a rail attached.

UCP, announced by Google and Shopify on 2026-01-11 and published under Apache 2.0 at github.com/Universal-Commerce-Protocol/ucp, does something structurally different. It defines the commerce conversation — discovery, cart, checkout, order, fulfillment — and then defines payment as a pluggable slot filled by specifications called Payment Handlers, which anyone who controls a domain can author. The protocol never names a rail.

That seam is where an infrastructure company should be looking. So we cloned the repo at commit a839e99 (2026-07-31), read the specification end to end, and probed the live endpoints. What follows is the architecture, and then what the audit turned up.

The profile is the API

UCP has no registry, no onboarding form, and no approval committee. A business publishes a single JSON document at /.well-known/ucp declaring which capabilities it supports, at which versions, over which transports, and with which payment handlers. A platform — an agent, an app, a shopping surface — publishes the same shape of document at a URL of its own and advertises it per-request through a UCP-Agent header.

The spec calls the result permissionless onboarding: any platform with a discoverable profile can transact with any business without a prior relationship. That is the same design instinct behind the MCP Registry's DNS-based namespaces, and it solves the same problem — how do you let strangers integrate without a gatekeeper.

Names are reverse-domain: {reverse-domain}.{service}.{capability}. The core shopping capabilities live under dev.ucp.shopping.*checkout, cart, order, fulfillment, catalog.search, catalog.lookup, discount, buyer_consent, split_payments, ap2_mandate. Anyone else uses their own domain: com.example.payments.installments.

What makes this more than a naming convention is authority binding. Every capability MUST declare a schema URL, and that URL's origin MUST match the namespace authority in its name. You cannot publish a capability under dev.ucp.* unless you serve its schema from ucp.dev. The spec is precise about what this buys you:

This binding guarantees provenance, not trust: a valid binding proves only that the reverse-domain name is controlled by the party that owns the corresponding domain. It does not assert that the entity is trustworthy, correct, or worth supporting.

Note the asymmetry: the schema URL is authority-bound because it is on the machine trust path, while the optional spec URL is documentation and MUST only be HTTPS. Provenance travels with the schema, not with the prose.

Negotiation happens on every transaction

Most integration protocols configure once and run. UCP recomputes on each session. Ilya Grigorik, the Shopify engineer who wrote the company's launch write-up, gives the reason plainly: "Change the cart, change the buyer's region, change any variable and the handlers may shift."

The business fetches the platform's profile, computes the intersection, and the result is the set of capabilities live for that session. The algorithm is four steps:

// Intersection algorithm

Name match, then version, then prune

1. Compute intersection. Include a business capability if a platform capability with the same name exists. Exact string match on the reverse-domain name.

2. Select version. Take the version strings present in both arrays. Non-empty, pick the highest (latest date). Empty, drop the capability entirely — there is no fallback.

3. Prune orphaned extensions. Remove any capability whose extends parent is absent. Single-parent needs its parent present; multi-parent needs at least one.

4. Repeat step 3 until nothing more is removed, which handles transitive extension chains.

Two consequences follow. First, extensions are just capabilities with an extends field, so the versioning surface is uniform — there is no separate extension lifecycle to reason about. Second, a name typo is not a degraded experience, it is silent absence. A capability that fails to match is simply not in the intersection, and neither side is obliged to say why. UCP separates this from transport failure carefully: a discovery failure (cannot fetch or parse the profile) is a transport error, while an empty intersection is a normal UCP response with an optional continue_url — the escape hatch that hands the buyer back to a human web checkout.

Payment handlers: the seam

UCP's payment model exists to solve an N-to-N problem between platforms, businesses and payment credential providers. It does it by separating two things that ACP fuses: payment instruments (what is accepted) from payment handlers (how instruments are processed).

A handler is a specification, not a company. Google Pay is the participant; com.google.pay is the handler it authors. The division of labor is explicit: the credential provider defines the schemas, the business configures the handler with its own keys and merchant IDs, and the platform executes the provider's protocol to acquire an opaque token. The business then charges that token through its existing backend relationship.

The spec calls the underlying structure the trust triangle: business and credential provider have a pre-existing legal relationship; platform and credential provider interact only to tokenize; platform and business exchange the result. Credentials flow platform to business only — businesses MUST NOT echo them back — and the handler_id in the payload tells the business which provider key to use, which the spec frames as preventing key confusion attacks.

Two operational rules are worth flagging because they are where implementations will get it wrong. Businesses MUST dynamically filter the handler list against cart context — pulling Buy Now Pay Later off a subscription cart, dropping regional methods that do not match the shipping address. And a checkout submission MUST carry exactly one payment instrument unless dev.ucp.shopping.split_payments is active; violations get a payment_failed in messages[].

The checkout state machine that wraps all of this is small: incomplete, requires_escalation, ready_for_complete, complete_in_progress, completed, canceled. complete_in_progress is the asynchronous window — the complete request was accepted but the order does not exist yet, which is exactly the window where a settlement layer underneath needs to be holding something.

The identity layer is machinery we have already seen

Here UCP converges hard with the rest of the field. All HTTP transports sign with RFC 9421 HTTP Message Signatures, body digests use RFC 9530 Content-Digest over raw bytes, keys are JWKs published in the same profile that carries capabilities, and replay protection is delegated to the business-layer idempotency-key. That is the identical foundation as Web Bot Auth and as Visa's TAP, which is not a coincidence — UCP explicitly designs for interop with both.

ES256 is the universal baseline every implementation MUST verify. ES384 and Ed25519 are OPTIONAL, and the kty/crv/alg vocabularies are open: a verifier that meets a key it cannot use MUST NOT reject the whole key set, it just cannot use that key. Algorithm choice is described as counterparty-driven — you pick what every audience your signature must satisfy will accept, and publish multiple keys only when no single algorithm does.

The dual-audience shape is the clever part. A signer can emit one signature that satisfies both UCP and Web Bot Auth, tagged web-bot-auth and accompanied by a Signature-Agent header. A plain UCP verifier resolves the key through UCP-Agent, treats signature-agent as an ordinary covered component, and never implements WBA key discovery at all. A WBA-aware verifier may instead resolve through Signature-Agent, in which case the keyid MUST equal the RFC 7638 SHA-256 thumbprint of the matched key.

Two rules in that algorithm deserve to be read twice. The first is a closed set of covered components that MUST be signed in every regime: @method, @authority, @path, @query when present, content-digest and content-type when there is a body, and the ucp-agent, signature-agent and idempotency-key headers when present. The spec states the attack it closes — this "prevents a signature satisfying only Web Bot Auth's minimal covered set from authenticating a UCP request whose body, method, or path is unbound." Web Bot Auth's minimum is @authority and signature-agent. Accepting that as authentication for a POST that places an order would be a hole.

The second rule — a key resolved via Signature-Agent proves control of that key source, not of the UCP-Agent profile, whose URL is "merely a signed header value." Implementations MUST treat the request as a single authenticated identity only when both URLs normalize to the same thing. Otherwise they are two identities and policy decides. That is the confused-deputy discipline the MCP authorization spec arrived at from a different direction.

AP2 mandates and the security lock

For autonomous purchases UCP does not invent a consent format. It hosts one: the dev.ucp.shopping.ap2_mandate extension carries AP2 credentials inside the checkout. All AP2 fields nest under a single ap2 object, which keeps the base schema clean and gives canonicalization one rule to follow.

Once the extension lands in the intersection the session is, in the spec's words, Security Locked — neither party may fall back to unprotected checkout. Concretely: the business MUST embed ap2.merchant_authorization in every checkout response, MUST NOT accept a complete request lacking ap2.checkout_mandate, and the platform MUST verify the business signature before showing anything to the user.

The merchant signature is a JWS with detached payload — <header>..<signature>, the double dot marking the payload as the checkout body itself. The signing input covers the encoded header as well as the payload, which the spec notes is what stops algorithm substitution: you cannot rewrite alg without breaking the signature.

// sign_checkout — payload is the checkout minus the ap2 field
payload  = checkout without "ap2"
canonical = jcs_canonicalize(payload)          // RFC 8785
header   = { "alg": "ES256", "kid": "merchant_2026" }

signing_input = b64u(header) + "." + b64u(canonical)
signature     = sign(signing_input, private_key)

checkout.ap2.merchant_authorization = b64u(header) + ".." + b64u(signature)

The platform then produces two SD-JWT credentials with key binding — the same primitive as the Verifiable Intent chain. The checkout_mandate goes in ap2.checkout_mandate and MUST contain the full checkout response including merchant_authorization, so the platform's signature covers the business's signature. The payment_mandate rides in payment.instruments[*].credential.token. One protects the terms, the other protects the funds.

Canonicalization is JCS (RFC 8785) rather than the raw-bytes Content-Digest used for requests, and the spec explains why in a way most specs do not bother to: mandates are durable evidence retrieved months later, they cross platform, business, PSP and card network — each of which may re-serialize the JSON — so any party must be able to reconstruct the signed bytes from logical content.

One honest footnote in the spec is worth repeating. AP2 v0.2 is internally inconsistent about mandate signing algorithms — specification.md requires a non-deterministic class such as ECDSA, while its security considerations state an entropy rule that any algorithm satisfies given enough payload entropy. UCP points at AP2 issue #268 and defers. Under the entropy reading, one Ed25519 key could serve both AP2 mandate signing and Web Bot Auth; under the algorithm-class reading it cannot, and you carry two keys.

What the audit found

The repo is genuinely alive. As of 2026-08-03: 3,259 stars, 432 forks, 236 commits from 54 contributors, 81 open issues and 79 open pull requests, with commits landing the same day we cloned. The top committers are the Shopify and Google engineers who wrote it, not a marketing team. Sibling repos — python-sdk, js-sdk, conformance, ucp-schema (a Rust validator) — are all under Apache 2.0 and all pushed within the last two weeks. This is not another announcement repo frozen after launch.

Three observations, in increasing order of how much they should affect your plans.

Released versus draft. There are three tags: v2026-01-11, v2026-01-23, and v2026-04-08 published on 2026-04-09. Since then, nearly four months of main-branch churn and 79 open PRs, with no new tag. Meanwhile mkdocs.yml sets ucp_version: "draft", so every schema URL in the documentation renders against /draft/. Both resolve live — https://ucp.dev/draft/schemas/shopping/checkout.json and https://ucp.dev/2026-04-08/schemas/shopping/checkout.json both return 200 — but they are not the same document, and "follow the docs" and "pin to the release" are currently two different integrations. The unversioned $id values in the repo source (https://ucp.dev/schemas/profile.json) are build-time placeholders rewritten to the versioned path at publish; fetched directly they 404.

A name that can never intersect. The canonical capability is singular — "name": "dev.ucp.shopping.ap2_mandate" in source/schemas/shopping/ap2_mandate.json. The interactive playground document declares it twice in the plural, dev.ucp.shopping.ap2_mandates, both in a profile fixture and in a capability list. Step 1 of the intersection algorithm is an exact match on name. A business that copies that fixture advertises a capability no platform will ever match, gets no error, and simply never enters AP2-protected checkout. It is a two-character bug in the one page implementers are most likely to copy from — the same class of defect as the sig1/sig2 mismatch we found in Visa's reference implementation, and far more fixable.

No stablecoin rail exists. This is the finding that matters for us. A full-text search of the specification and schemas at a839e99 returns zero matches for x402, stablecoin, or USDC. The only hits for "crypto" are cryptographic libraries and an EMV cryptogram field. The demonstrated handlers are com.google.pay, dev.shopify.shop_pay and a mock. Even Scenario C — the section literally titled "Autonomous Agent (AP2)", the recommended flow for agents — submits "credential": {"type": "card", "token": "..."}. The agent gets cryptographic mandates; the money still moves over a card.

What it means for LLM4Agents

UCP is not competing with x402, and treating it as a competitor would be a category error. It standardizes the commerce conversation — what is in the cart, what it costs, who consented, where it ships. x402 standardizes the settlement of a single HTTP request. A UCP checkout could settle over any rail its handler names.

What UCP changes for us is the shape of the opportunity. ACP fused checkout and payment, so adding a stablecoin rail to ACP means arguing with OpenAI and Stripe about their token. UCP put a documented seam exactly where a rail plugs in, and made the namespace permissionless: a payment handler is a reverse-domain name plus a schema served from the matching origin. There is no committee to petition. Anyone who owns a domain can author a handler and any business can advertise it.

The asymmetry cuts the other way too. UCP's checkout is retail-shaped — line items, shipping destinations, tax, returns, warranty policies, loyalty. Our traffic is machine-to-machine: an agent buying inference, storage, a tool call. Those do not need a cart. The realistic overlap is not "agents buy widgets through our gateway"; it is that a UCP-native agent already speaks a profile, already signs RFC 9421, already resolves keys from a well-known document. That agent is one handler away from paying us the way it pays a merchant, and every piece of identity plumbing it built for UCP is plumbing it does not have to build for x402.

The threat is narrower and worth naming. If the card networks' handlers become the only ones with production deployments, "agentic payment" ends up meaning "tokenized card" by default, and stablecoin settlement stays a niche for API-shaped goods. The seam being open is not the same as the seam being used.

Staying on the frontier

Four things, in order.

Publish a payment handler. We control llm4agents.com, which is all UCP's authority binding requires. A com.llm4agents.x402 handler — config schema for the accepted networks and asset, instrument schema for an x402 payment payload, executed by the platform against our facilitator — is a small, self-contained artifact that costs a domain and a JSON file. It is also the cheapest possible way to have the first stablecoin handler in an ecosystem that currently has none. Ship it as a draft, link it from the docs, and open an issue upstream rather than a PR: authority binding means it does not need to live in dev.ucp.* to be legitimate.

Serve a profile. Publish /.well-known/ucp declaring our identity keys before declaring any shopping capability. The profile is a JWK Set by construction, and the same document serves UCP key lookup and Web Bot Auth jwks_uri resolution. That is one artifact satisfying three specs — UCP, WBA, and the directory shape Visa's TAP expects — and it is the prerequisite for every later step.

Sign with the dual-audience shape. When our agents call out, emit RFC 9421 signatures that satisfy UCP's closed covered-component set — method, authority, path, query, content-digest, content-type, plus ucp-agent and idempotency-key. That set is strictly stronger than WBA's minimum, so a signature built to UCP's bar passes WBA verifiers too. Building to the weaker bar and retrofitting later means resigning everything.

Map the state machine to reserve-then-settle. complete_in_progress is a checkout that has been accepted and not yet ordered — the same window our reserve → proxy → settle cycle already models, and the same window the upto scheme covers when the final amount is not known at authorization time. If we implement UCP checkout, the reserve should open at ready_for_complete and settle on the terminal transition, not before. The protocol gives us the state names for free; the discipline of not settling early is ours to keep.

UCP is the first of these protocols where the useful move is not a comparison post. It is a JSON file served from a domain we already own.

Pay per call, in stablecoins, over an OpenAI-compatible API

No cards, no contracts, no minimums. Your agent registers, deposits, and calls.

Register an agent