Treeship
Commerce

Claude Commerce Agents

Wire tamper-evident receipts into anthropics/commerce-agents on the Messages API, the Agent SDK, and Managed Agents, verify a session offline, and know exactly what a receipt does and does not prove.

anthropics/commerce-agents is Anthropic's reference blueprint for two agents on Claude: a shopping agent a business embeds for customers and a merchant agent its staff use for the back office. Each runs on three paths, the Messages API, the Claude Agent SDK, and Managed Agents, over one set of tool contracts, gates, and skills.

The reference draws a clear line in its docs/safety.md. Enforced in code: fencing of third-party content, provenance gates on every write, caps, memory validation, and a host-approval gate on merchant changes. Left to the deployment: the approval surface, payment, and log hygiene. treeship-commerce is what a deployment adds for the record of what happened. It records; the reference's gates still decide what runs.

Anthropic publishes the reference as a reference: it is not maintained and does not accept contributions. treeship-commerce is a separate package that wraps it through the executor_class seam every runtime already exposes. No line of the reference changes.

Why one wrapper covers three runtimes

Every tool call, on every path, passes through one method: commerce_common.execution.BaseToolExecutor.execute. The reference relies on that for its own guarantees; its safety table says "a rule enforced inside a tool call holds on all three paths." TreeshipExecutorMixin overrides that one method:

  1. Intent receipt, signed, before dispatch: the tool name, a SHA-256 of the canonical arguments, the role, and the session tag.
  2. The tool, exactly as the reference runs it: validation, gates, handler, fencing.
  3. Result receipt, signed, after: status (ok, blocked with the gate's name, or error), a SHA-256 of the result text, the event types the tool emitted, elapsed time, and whether the intent was recorded.

Each receipt names its parent. A session reads intent → result → intent → result … from the Treeship session's root artifact, and treeship verify walks it as one chain. A call the provenance gate holds is a signed refusal: the receipt exists, says blocked, and names provenance.

Anatomy of a receipt

Both receipts below are real, from python -m treeship_commerce.demo on a fresh ship. This is the add-to-cart the reference's provenance gate held, because the product id never came from a catalog read in that session.

The intent, decoded from its DSSE envelope:

{
  "type": "treeship/action/v1",
  "timestamp": "2026-09-06T19:33:00Z",
  "actor": "agent://shopping",
  "action": "commerce.tool.add_to_cart.intent",
  "parentId": "art_21f62db3cbb2f1d839262a0d672b503b",
  "meta": {
    "args_digest": "sha256:bbb323b035288b725c4c593fb5c1ee9422d4ff43d18a928dc3e24ae9562c6ad1",
    "role": "shopping",
    "session_tag": "ea993c8c62ea",
    "tool": "add_to_cart"
  }
}

The result, whose parent is that intent:

{
  "type": "treeship/action/v1",
  "timestamp": "2026-09-06T19:33:00Z",
  "actor": "agent://shopping",
  "action": "commerce.tool.add_to_cart.result",
  "parentId": "art_57b39c3177d8bf4d95002b9c25344149",
  "meta": {
    "status": "blocked",
    "gate": "provenance",
    "result_digest": "sha256:3ab4b78a4205d1c7dd613f0cd8860e5bb5ae767026ab5191f281f41530404308",
    "events": [],
    "elapsed_ms": 0,
    "intent_recorded": true,
    "role": "shopping",
    "session_tag": "ea993c8c62ea",
    "tool": "add_to_cart"
  }
}

Around each statement sits the standard Treeship record: the DSSE envelope with the Ed25519 signature, payload_type: application/vnd.treeship.action.v1+json, the signing key_id, and the content-addressed artifact_id (art_ + the first 16 bytes of SHA-256 over the signed bytes). Change one byte of the statement and the id no longer matches.

What is deliberately not in a receipt:

Not writtenWhy
The argumentsA reader with the arguments can recompute args_digest; a reader with the receipt learns nothing about them.
The result textOn the reference it is fenced third-party content (catalog rows, reviews, policies).
The commerce session idThe reference treats it as the request credential and logs only a twelve-hex tag. The receipt carries that same tag, from the reference's own session_tag helper, so an operator holding the id can correlate and nobody else can.

Install

git clone https://github.com/anthropics/commerce-agents && cd commerce-agents
python3.11 -m venv .venv && source .venv/bin/activate   # the reference needs Python 3.11+
pip install -r requirements.txt                  # the reference's packages; unregistered on PyPI by design
pip install treeship-sdk treeship-commerce
curl -fsSL https://treeship.dev/install | sh     # the CLI does the signing; the demos also fetch it themselves
treeship init --config .treeship/config.json     # a workspace for this directory

Or let Claude Code do the wiring. The treeship-commerce plugin mirrors the reference's own commerce-builder: one command that reads your project, wraps the executor on your runtime, signs the merchant approval surface or the shopping checkout hand-off, and records what it decided in your CLAUDE.md decision record.

claude plugin marketplace add zerkerlabs/treeship
claude plugin install treeship-commerce@treeship
# then, in your project:
/add-treeship-receipts merchant      # or shopping, or both

Wire it

Open a Treeship session around the commerce session

from treeship_sdk import Treeship
from treeship_commerce.lifecycle import start_session

ts = Treeship()
root = start_session(ts, name="storefront:acme", actor="agent://shopping")

root is the session's root artifact id, the parent every tool receipt chains from. start_session and close_session take a cwd; the CLI finds the workspace by walking up from the working directory, so a host that runs elsewhere passes the directory it initialized.

Build the receipted executor class

from treeship_commerce import TreeshipReceipts, receipted
from shopping_agent.executor import ShoppingToolExecutor

ReceiptedShopping = receipted(
    ShoppingToolExecutor,
    recorder=lambda ex: TreeshipReceipts(
        ts,
        actor="agent://shopping",
        session_id=ex._session.session_id,   # tagged, never written
        parent_id=root,
    ),
)

receipted puts the mixin first in the MRO. The recorder factory runs once per executor on its first tool call, so each executor, which the reference builds per session, gets its own chain and its own tag. MerchantToolExecutor wraps the same way with actor="agent://merchant".

Hand it to the runtime you use

Messages API (shopping_agent_runtime.ShoppingAgent):

agent = ShoppingAgent(
    backend=your_backend,
    skills_dir=Path("shopping-agent/skills"),
    config=ShoppingAgentConfig(brand_name="Your Store"),
    executor_class=ReceiptedShopping,
)

Agent SDK (shopping_agent_sdk.ShoppingToolset):

toolset = ShoppingToolset(backend=your_backend, executor_class=ReceiptedShopping)

Managed Agents (the storefront MCP server, build_server):

server = build_server(backend=your_backend, config=config, executor_class=ReceiptedShopping)

On Managed Agents the executor lives inside your MCP server, one per client connection, so the receipts are written where your backend runs, next to the credentials the model never sees.

Seal the session

from treeship_commerce.lifecycle import close_session

sealed = close_session(ts, summary="14 tool calls, 1 held by the provenance gate, checkout handed off")
sealed["package"]      # .../.treeship/sessions/ssn_….treeship

treeship session report publishes the package to a hub and returns a permanent receipt URL; that step is a deliberate, separate action.

If you construct executors yourself, attach(executor, receipts) works on an instance of a receipted class. It refuses a plain reference executor, because one that silently recorded nothing would be the exact failure this exists to remove.

Verify a session

Offline, from the CLI, against your own trust roots:

treeship verify art_44180341eaeafa79d472115c7c38b03a       # the chain head
#   ✓ verified  (11 artifacts . chain intact)
treeship package verify .treeship/sessions/ssn_a52cd0de94f479d3.treeship
#   18 passed, 0 failed, 1 warnings
#   ✓ package verified

--format json gives a CI-consumable document: every check by artifact id, chain_linkage_ok, and the outcome. A published session renders at treeship.dev/receipt/<id> with the timeline and the same verifier running in the browser (@treeship/verify, WebAssembly); the hub stores bytes and serves proofs, it never issues a verdict.

The demo's output, from a clean ship with the released 0.30.0 CLI and the SDK from PyPI (the 1 warnings above is the package's always-on note that the narrative fields are not signature-bound; the artifacts and Merkle root are):

tool calls        intent-id result-id
  search_products        ok                   art_2e3bde5a65b7e4ed… art_a449ab98b03749ca…
  get_product_details    ok                   art_dc850843d507bc28… art_9c594ed1a4accd5f…
  add_to_cart            ok                   art_56a530661bb790df… art_3a7ac26825f5468d…
  add_to_cart            blocked:provenance   art_5eb807317d55435b… art_a58a450ef62841c9…
  checkout               ok                   art_235440f60e9061ef… art_cd10ddb427c8df94… art_941690f65293e228…
  checkout hand-off      cart signed          art_cd10ddb427c8df94…
  order placed           chained              art_e8aad1f89cb61f4e…

session           receipts=11 events=6 root_verified=True

The same run, recorded. The fourth call is a product id the agent never saw in a catalog read, pushed at the cart the way a poisoned listing would push it. The reference's provenance gate refuses it; the recording then decodes that receipt's signed bytes to show "status":"blocked" and "gate":"provenance" inside the signature, and treeship package verify proves the refused add is in the sealed tree.

The shopping demo: a search, a product read, an add, an add the provenance gate refused, the hand-off and the chained order; the refused add's signed receipt decoded; the sealed package verifying offline with that receipt's inclusion proof passing

To verify a package someone hands you, without the ship that produced it, see Verify a session package.

What a receipt proves, and what it does not

Proves. That this ship's key signed, at that time, the statement that add_to_cart was about to run with arguments of that digest, and then that the provenance gate held it. That the receipts form an unbroken chain from the session root. That nobody edited any of them after the fact, on any machine, checkable without contacting anyone.

Does not prove. That the tool's answer was correct, that the catalog was truthful, or that the customer got what they wanted. A wrong answer with a perfect receipt is still wrong. Treeship authenticates statements; it does not adjudicate commerce.

Trust boundary. The signing key lives on the machine that runs the executor. That is the right place for a deployment's own record of its agent, and it is a boundary: root on that machine can sign anything. A counterparty who wants to trust these receipts pins your ship key once (treeship keys export prints the line) and verifies against it.

Checkout: a receipt for exactly the cart that went to checkout

The reference's checkout tool takes no cart and never shows the model a payment URL. When the model calls it, the presentation layer reads the cart from the backend and asks backend.checkout_handoff(session, cart) where that cart is paid for: the platform's hosted checkout URL, or one per seller on a marketplace. The URL goes on the card the host renders and nowhere else. That call is the one moment "this cart" and "this checkout" meet, inside the backend the deployment owns.

receipted_backend(backend) wraps a storefront backend so that call also signs a hand-off receipt: a digest of the cart's lines, the item count, subtotal and currency, and a digest of each URL handed off (with the seller when there is one). It sits on the session chain between the checkout intent and its result. The URL itself is never written; a holder of it can check the digest, a reader learns nothing. The cart's lines are never written either; a holder of the checkout card can recompute the digest from the card's own cart payload, and the tests do exactly that.

from treeship_commerce import receipted_backend, order_placed

backend = receipted_backend(MyStorefront())     # once, where the runtime is built
# ... the runtime and the receipted executor as before ...

# later, when the host has placed the order on its own checkout page:
await order_placed(receipts, order_ref=order.order_id, amount=order.total, currency="USD")

order_placed is the host's half of the seam. It signs an order receipt onto the chain after the hand-off, naming the hand-off in meta.handoff (never onto the hand-off itself, which would fork the chain and leave the checkout result out of the sealed package): a digest of the order reference (never the reference itself, which is the customer's lookup key), the amount and the currency. A customer, a merchant, or an auditor holding the cart can recompute its digest and check that the order chains from a hand-off for exactly that cart, offline. When the host never wrapped its backend, the order receipt still lands on the session chain and says handoff_recorded: false rather than pointing at something invented.

The wrapped backend finds the right recorder by the commerce session: every TreeshipReceipts registers itself under its session tag, and the backend looks the session up the same way. That is the default and needs no wiring beyond the receipted(...) executor class; pass recorder= to override it.

The shopping demo now ends with the hand-off and a mock order chained onto it:

  checkout               ok                   art_… art_…
  checkout hand-off      cart signed          art_…
  order placed           chained              art_…

Approvals: the operator's yes, spendable once

The merchant side has one tool that mutates a merchant's system of record, apply_change, and the reference already gates it well: the change must have been staged in this session, must pass the configured guardrails, and, when require_host_approval is on, must be in state.approved_change_ids. The console's y/N puts it there with toolset.host_approve(change_id) and takes it out with host_clear when the apply turn returns.

That set is an in-process boolean. It is right while the process runs and gone afterwards, so it cannot answer the two questions an operator is asked later: did a human approve this, and when? and was that approval used more than once? MerchantApprovals answers both. When the operator says yes it mints a signed Approval Grant scoped to one actor, one action, and one change, with max_uses=1. The apply's intent receipt is then signed with that grant's nonce, which makes the CLI reserve a use in the local Approval Use Journal before it will sign. A second apply of the same change finds the grant spent. A grant for one change cannot be spent on another, because the action names change://<change_id> as its subject and the grant's scope names the same URI. The subject comes from the call's own arguments, never from the grant, so the scope check cannot compare a grant against itself.

from treeship_commerce import MerchantApprovals, approved, receipted
from merchant_agent.executor import MerchantToolExecutor

approvals = MerchantApprovals(ts, approver="human://operator", actor="agent://merchant")
Executor = approved(receipted(MerchantToolExecutor, recorder=make_recorder), approvals)

# in the host's y/N loop, where it currently calls host_approve alone:
approvals.grant(change.change_id, summary=change.summary)   # the evidence
toolset.host_approve(change.change_id)                      # the reference's own gate

Both calls, because they answer different questions: the mark lets the apply run, the grant proves it was allowed to. The apply's intent receipt then carries approval: "proven", the grant's artifact id, and the change id. When a grant is missing, spent, expired, or scoped to another change, the receipt is still written and says approval: "unproven" with the CLI's reason (would exceed max_uses (1/1), scope refused this action: subject ...). The tool still runs into the reference's check_apply_change, which is what holds it. Pass enforce=True to approved(...) to hold an unproven apply before the tool runs, using the reference's own held-outcome shape.

treeship approval uses <grant-id>       # the journal's record of the one use
treeship approval journal verify        # the journal's own hash chain

treeship package verify on the sealed session reports the same evidence as a check of its own: PASS replay-local-journal -- local Approval Use Journal passed, use 1/1.

Per runtime

The three runtimes differ in where the human's yes enters the process, so they differ in what can be signed.

  • Agent SDK. MerchantToolset is the approval surface: the console calls host_approve on yes and host_clear when the apply turn returns. approving(toolset, approvals) wraps those two methods on the toolset you already built, so the console's loop is unchanged and every yes mints a grant, every clear forgets it:

    from treeship_commerce import MerchantApprovals, approved, approving, receipted
    
    approvals = MerchantApprovals(ts, approver="human://operator")
    toolset = approving(
        MerchantToolset(backend=backend, executor_class=approved(
            receipted(MerchantToolExecutor, recorder=make_recorder), approvals)),
        approvals,
    )
    # main.py's confirm_staged_changes() runs as-is.
  • Messages API. MerchantAgent leaves state.approved_change_ids to the host. Where the host adds the id, it also calls approvals.grant(change_id, summary=...). The executor the orchestrator builds per turn picks the grant up through executor_class.

  • Managed Agents. The MCP server runs with require_host_approval=False because the platform's always_ask prompt is the approval surface, and that click happens outside the process. Nothing here can sign it. An apply there is receipted, and its receipt carries no approval claim rather than a fabricated one; enforce=True on that runtime would hold every apply, which is the honest reading of "no signed approval".

All three are exercised in the test suite through each runtime's own executor_class seam, the way a deployment wires them.

Requires treeship-sdk 0.29.0 or later, whose attest_action takes subject; treeship-commerce 0.29.0 declares that floor. On an older SDK grant() warns once and mints nothing, and the apply stays gated exactly as the unwrapped reference gates it.

The merchant demo runs all of it with no model and no API key: a staged price change held unapproved, applied once under a signed grant, and refused on replay while the reference's own mark is still set.

python -m treeship_commerce.demo_merchant

Verifiable Intent at hand-off

Verifiable Intent is the open draft at verifiableintent.dev, still marked v0.1, for proving an agent was authorized to buy: an issuer credential, a user mandate that delegates to the agent's key with constraints, and a Layer 3 pair the agent signs for the network and the merchant. The published constraints page registers eight types. The protocol landscape maps the credential onto AP2's open verifiable-digital-credential slot and says it can ride in UCP's AP2 mandate extension and in ACP's extension. Treeship does not speak those transports. treeship vi attest signs the Layer 3 pair with the spec's optional agent_attestation claim pointing at this receipt chain. That claim is the behavioral record the spec's security model says v0.1 does not provide on its own. Run at hand-off, the chain head it names is the hand-off receipt, so the credential binds the cart that went to checkout to the mandate that authorized it.

from treeship_commerce import attest_at_handoff, vi_verify

summary = attest_at_handoff(
    ts, receipts,
    mandate=l2_sdjwt, checkout_jwt=merchant_jwt,
    merchant="merchant-uuid-1", items=[("BAB86345", 1)],
    amount_minor=27999, currency="USD",
    aud_network="https://www.mastercard.com", aud_merchant="https://tennis-warehouse.com",
    out="./vi-out",
)
summary["attestation"]["chain_head"] == receipts.last_handoff    # True
vi_verify(ts, mandate=l2_sdjwt, out="./vi-out")["outcome"]       # "pass"

The same path, driven over the retail mock with a local wallet:

python -m treeship_commerce.demo_vi

treeship_commerce.wallet issues Layer 1 and Layer 2 with the reference SDK's published demo keys and a demo token (mastercard.srcDigitalCard, not a live card). The agent key comes from treeship vi keygen. The run buys the ACME Basecamp tent the catalog search returns, holds an add the provenance gate refuses, signs L3a for https://www.mastercard.com and L3b for the merchant, chains the order onto the attestation, and checks the pair with treeship vi verify --local. No card is charged. Needs the reference SDK as well as commerce-agents.

A second run is the buy side of a machine payment, not that credential. One budget, a localhost seller, HTTP 402:

python -m treeship_commerce.demo_402

The budget is 500 minor units. Three requests fit. Each one is answered 402, signed as payment.voucher.sign, checked by the seller with treeship verify, and followed by a payment.delivery receipt. A fourth amount would pass 500, so the agent signs payment.voucher.withheld and no voucher. The scheme is local.402.v1. The seller delivers the resource and does not validate a network voucher, redeem one, or settle. Needs the treeship CLI. It does not need the commerce blueprint or the Verifiable Intent SDK, and it does not call Mastercard Agent Pay for Machines.

The purchase values must describe the cart. The mandate check runs before anything is signed; a purchase outside it raises and writes nothing. The attestation is a receipt (vi.l3.attested) on the chain after the hand-off, and order_placed chains the host's order onto it, so the sealed package holds the whole story: the calls, the refusals, the cart, the credential, the order. What goes to the network (l3a.sdjwt with l2-payment.sdjwt) and to the merchant (l3b.sdjwt with l2-checkout.sdjwt) is in --out. Needs treeship 0.31 or later. The current release is 0.31.14. Since 0.31.6, treeship vi verify --local --require-attestation checks the attestation signature under a trusted key.

Operating it

  • Keep the preimages. A receipt carries digests, never the arguments, the result text, the cart's lines, or the payment URL. That is the right privacy posture, and it means a receipt alone proves that something with this digest ran. To settle a dispute you also need the original: the arguments and result keyed to the receipt's artifact id, the cart that went to checkout, the order reference. Keep them in your own store, under your own retention and access rules, keyed by artifact_id (or by the receipt's session_tag plus timestamp when you already log that way). The reference's DEBUG logs contain request bodies and are one such store; its safety page says what retention they need. Treeship deliberately does not keep this for you: it would put customer data under a key that is not yours. When someone presents the original, args_digest, result_digest, cart_digest and order_ref_digest are recomputable with the functions in treeship_commerce (args_digest, text_digest, cart_digest), and the receipt then proves this ran, not something like it.
  • Recording never breaks the agent path. A receipt that cannot be written (CLI missing, ship not initialized, disk full) warns once, the tool runs anyway, and TreeshipReceipts.dropped counts it. A result whose intent is missing says intent_recorded: false and chains from the previous head; no id is ever invented.
  • TREESHIP_DISABLE=1 turns recording off. TREESHIP_DEBUG=1 logs every drop instead of the first.
  • Cost. Two CLI invocations per tool call, run off the event loop in a thread; tens of milliseconds each on a laptop. The timeline event is a third, cheaper append.
  • Log hygiene. The receipt's session_tag is the reference's own session_tag(session_id) when commerce_common is importable, so your log lines and your receipts correlate on the same twelve hex characters.

What comes next

  • A plugin command in the reference's own marketplace format, alongside its /add-commerce-flow.

Reference

  • Package: integrations/commerce-agents/; on PyPI as treeship-commerce
  • Tests: thirty-seven cases on a real ship over the reference's retail and merchant mocks, run in CI against the reviewed commit of the reference; eight cover approvals, including the replay and the wrong-change refusals, four drive the merchant side through each runtime's executor_class seam, seven cover the checkout hand-off and the chained order, one drives the wallet demo through search, a provenance refusal, and a signed Layer 3 pair, and one drives a local HTTP 402 under one budget: three signed vouchers and a fourth payment withheld with no voucher
  • Related: Integration overview, Agentic commerce, Payment proofs, Trust model