Migrating to 0.2.0
0.2.0 is a minor release that, under the versioning policy, is allowed to break you — and names every break. Most integrations are unaffected; a few that implement a custom store or call the ledger directly need small, mechanical changes. This page is both halves: what stays the same, and what to change.
If you keep the defaults, nothing changes
Section titled “If you keep the defaults, nothing changes”A single-process integration that uses the default MemorySpendStore and sets neither maxTotal
nor recipientPolicy behaves exactly as it did in v0.1. The new features are opt-in, the default
policy is unchanged, and no configuration you already have means something different. You can upgrade
and move on.
The rest of this page matters if you implement a custom SpendStore, call the ledger
directly, use the async Python client, or want to turn the per-process guardrails into a
fleet control plane.
Breaking changes, and what each asks of you
Section titled “Breaking changes, and what each asks of you”The full list is in the changelog; here is what to actually do about each.
-
The
SpendStorecontract is now v2. A custom store must addexpose,listExposed,isFrozen, and acapabilitiesproperty ({ atomicGlobalFreeze }), andreservemust return aReserveSpendResult({ reservation, recipientPinEstablished }) rather than a bare reservation. Run the published conformance suite against your store — it enforces every new member and the atomicity the whole design rests on. It ships in both packages, off the core path, so importing it costs a core install nothing:import { checkSpendStore } from "tx402/spend-store-contract";from tx402.spend_store_contract import check_spend_store``` **Most people should notimplement a store at all in 0.2.0** — the [Redis, Durable Object, and gateway referencestores](../../operations/shared-store/) exist so you don't have to. -
Lifecycle operations take a
ReservationRef.release,expose, and adminresolveExposedaccept{ reservationId, policyScope, assetId }, andCommitSpendInputgainspolicyScope/assetId. An in-process caller passes theSpendReservationit already holds; a custom store addresses records by the full triple, not a bare id. -
commiton an expired reservation is now refused (expired-cannot-commit) where v0.1 permitted it. If you relied on the old permissive behaviour, commit before the reservation expires, or reconcile through the exposure path instead. -
Durable stores window on their own clock.
BudgetQuery.nowEpochMsis advisory for a durable store — it computes the window from backend time, so a query for a past instant works only onMemorySpendStore. If a test advanced time by passing anowEpochMs, drive the store’s own clock instead. -
AsyncTx402Client.get_budget_state(...)is nowasync def. Add anawait. The sync client is unchanged. -
Recipient checking adds a fixed policy step. Observable only when
recipientPolicy.modeis not"off", so it does nothing until you opt in. -
Python
payTois now bounded at ≤ 128 characters, matching TypeScript. A conformant address is well under that; only a malformed one is now rejected earlier. -
Two new error codes,
TX402_SPEND_FROZENandTX402_RECIPIENT_UNPINNED(both exit3). If you match the taxonomy exhaustively, add the two arms; both are new refusals your code could not have produced before.
Single process → a fleet on one budget
Section titled “Single process → a fleet on one budget”This is the reason to upgrade. The path, in the order to do it:
- Stand up a shared store. Construct a reference store and pass it as
spendStore/spend_store, keeping the normalized-host scope you already use. The shared-store runbook does this on Redis; a Cloudflare deployment uses the Durable Object, and the recommended production boundary is a gateway in front of either. - Add a cumulative ceiling. Set
policy.maxTotalfor a lifetime bound, not just an hourly rate. - Administer the caps in the store, through the admin plane, so a drifted worker cannot widen them — this is what turns “trusted identically-configured clients” into an enforced guarantee (see administered caps).
- Provision two credentials. A data-plane credential for the agents and an admin credential for operators; the agents must not hold the admin one. On a gateway these are two bearer tokens; on raw Redis, two ACL users.
- Enable the kill switch and learn the
tx402 freezerunbook. - Pin recipients per merchant — allowlist first, or TOFU with a pin store — and document that a
legitimate recipient change now needs
tx402 rotate-recipient. - Adopt the exposed-reconciliation loop. A shared cumulative cap means a maybe-settled payment holds budget until you reconcile it; this is the one new manual day-2 task, and it is what keeps a fleet from ever spending past its ceiling.
examples/python/fleet.py
is steps 1–5 in one runnable file.