Agentic Commerce Protocol: a checkout, approved once
Agent Runtimes speaks the
Agentic Commerce Protocol
(OpenAI, Stripe) 2026-04-17 as a buyer, in Python and TypeScript, over
REST. In code it is agentic_commerce, never acp: acp is the Agent
Client Protocol, an editor protocol Agent Runtimes also speaks.
| Python | TypeScript | |
|---|---|---|
| Package | agent_runtimes.protocols.agentic_commerce | @datalayer/agent-runtimes/lib/protocols/agentic-commerce |
| Models | generated from spec/2026-04-17/json-schema/schema.agentic_checkout.json by datamodel-code-generator 0.34.0 | generated by json-schema-to-typescript 15.0.4 |
| Plugin | agent-runtimes.protocols.agentic-commerce | AgenticCommercePlugin |
No maintained client exists from the specification's authors (the
agentic-commerce-protocol package of PyPI and npm is a third party's), so
the models are generated here, pinned to the dated version:
scripts/codegen/generate_agentic_commerce.sh [VERSION] regenerates both.
A purchase is a proposal
As with UCP, nothing is bought in one call: a ready_for_payment
session becomes an exact, revisioned proposal the person approves, completed
once, the proposal's id its Idempotency-Key:
client = AgenticCommerceClient(
seller="https://outfitters.example",
credentials=credentials_of(platform),
extensions=declared_extensions(platform),
)
session = await client.create_checkout([{"id": "socks", "quantity": 2}], "usd")
session = await client.update_checkout(session["id"], {"buyer": {"email": "ana@example.com"}})
proposal = await client.propose_completion(session["id"])
order = await client.complete(
proposal,
approver=approver_of(platform),
payment=payment_data_of(platform),
store=proposal_store_of(platform),
)
Every request carries Authorization: Bearer (the agent's credential with
that seller, from the host), API-Version: 2026-04-17, and on every POST an
Idempotency-Key.
One item per unit. The 2026-04-17 schema's Item has no quantity and
forbids other fields, while the specification's examples send one; the
schema is followed: a quantity of two is the item twice, and the seller
counts them.
The plugin and its points
| Point | Default | What a host contributes |
|---|---|---|
agentic_commerce.approver | none: nothing is bought | the same approver it gives ucp.approver |
agentic_commerce.paymentData | none | a delegated payment token (spt) from custody |
agentic_commerce.credentials | none | the agent's bearer with each seller |
agentic_commerce.proposalStore | memory | a store shared by its processes |
agentic_commerce.extension.discount | none | the discount extension |
agentic_commerce.extension.other | none | any other extension, by name |
An agent declares in capabilities.extensions only the extensions
contributed to these points (declared_extensions); the seller answers
with those it activates.
In an application
An application lists acp in protocols and names each merchant in its
commerce block (commerce.merchants[]: protocol: acp, the merchant's
domain, a level and, to buy, the most one purchase may cost, limit). For
now Datalayer shops at test merchants only, with no real payment
(decided 2026-10-13). On Datalayer the runtime holds nothing of it: each
merchant is one server, acp_<domain> (acp_outfitters_example), which is the
MCP gateway's acp toolset (/mcp?only=acp&acp-merchant=<domain>)
reached in the person's name; its tools are named with it
(acp_outfitters_example_acp_complete).
The application's Rules decide those tools by what agentspecs says each does
(protocol-tools/agentic-commerce.yaml): reading reads, filling a checkout writes,
acp_complete buys. A purchase is always asked of the person, by the
gateway, with the exact checkout — whatever the rule on buying says, the
runtime lets the call reach the gateway rather than ask twice — and made at
most once; Leave it to me buys nothing. A connection that reads gives only
what reads.
Reading a seller's discovery
AgenticCommerceClient.discover() reads /.well-known/acp.json through
agent_runtimes.protocols.egress.metadata_fetch, the PAP SDK's resolving
client: the name is resolved first and refused unless https with every
address public, only that address is dialled, no redirect is followed, no
proxy is used, and the document is bounded (256 KiB). A local test seller is
read over the client you trust by saying so — metadata=client_fetch(http)
(test_protocols_metadata_egress.py).
The test seller and the example
agent_runtimes.protocols.agentic_commerce.merchant.ExampleOutfitters is a
local seller (discovery, checkout sessions, the discount extension), holding
an agent to Authorization, API-Version and Idempotency-Key (409 on a
reused key with another body); every answer is validated by the generated
models.
Example
Runs locally, against the test seller above, in one process with no network and no model (example EX-05):
python examples/protocols/agentic_commerce/checkout.py --yes
It prints the seller's discovery (outfitters.example speaks the Agentic Commerce Protocol 2026-04-17, the discount extension), a session
not_ready_for_payment until the buyer's email is given, the WELCOME10
discount applied (ready_for_payment), the exact purchase to approve (Buy
2 × Wool socks at outfitters.example for USD 29.16), the order once bought,
and the same proposal refused a second time. Without --yes it asks you to
approve. The TypeScript client runs against a seller answering with the
shared fixture (agenticCommerce.unit.test.ts) and against the Python seller
itself over HTTP: seller_server.py serves Example Outfitters on localhost
and acpSeller.integration.test.ts buys there (a session, the buyer's email,
the purchase approved and completed once with a test token no summary shows,
the same proposal refused); test_protocols_agentic_commerce_two_languages.py
runs both.
python examples/protocols/agentic_commerce/seller_server.py --port 8813
ACP_SELLER=http://127.0.0.1:8813 npx vitest run --project=integration \
src/protocols/agentic-commerce/acpSeller.integration.test.ts
Conformance
Agentic Commerce 2026-04-17. agentic_commerce/fixtures/session.json (a session, the discovery document, the revision digest) is read by test_protocols_agentic_commerce.py and agenticCommerce.unit.test.ts; both validate with models generated from the same JSON Schema.
All protocols side by side: Conformance.
What never reaches a model
The payment token and the bearer go to the seller and nowhere else:
session_summary and proposal.summary() are what an agent sees, and the
tests check that neither secret is in them.