ATP Consistency & Semantics
The Agent Transaction Protocol gives you at-least-once delivery, exactly-once effect. This page makes that promise precise: states, transitions, retries, receipt timing, and partition behavior.
(intent_id, idempotency_key) is
UNIQUE in the transactions table. A receipt is only emitted after the row transitions to
settled and is signed by the site key. There is no "maybe paid" state.
1. Intent state machine
Transitions are monotonic. Once a row leaves a state it does not return. There is no
executing → authorized edge. Recovery from a crash always re-enters at the same or a later state.
2. Delivery guarantee
The wire protocol is at-least-once: agents may retry any step until they observe a terminal state. The application layer turns this into exactly-once effect using the idempotency key:
-- transactions table (canonical shape)
CREATE TABLE transactions (
intent_id TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
state TEXT NOT NULL,
body JSON NOT NULL,
receipt JSON,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (intent_id, idempotency_key)
);
A retry with the same (intent_id, idempotency_key) returns the existing row verbatim —
same body, same receipt, same signature — without invoking the side-effecting handler again.
3. Idempotency key rules
- Required on every
POST /api/atp/authorizeandPOST /api/atp/execute. - Opaque to the server: any 16–128 char string the agent chooses. UUIDv4 is fine.
- Bound to the intent: the same key used against a different
intent_idis a different transaction. Cross-intent reuse is allowed. - Stable across retries: the agent must persist it before the first attempt. If the agent loses the key, it must abandon the intent and let it expire — it must not retry with a fresh key, which would double-execute.
4. Receipt timing
Receipts are issued only on entry to settled or compensated. They are never issued speculatively.
| State | Receipt visible? | Signed? |
|---|---|---|
| created → executing | No | — |
| executed | No (pending) | — |
| settled | Yes | Ed25519 over JCS canonicalization of the receipt body |
| compensated | Yes (compensation receipt) | Ed25519, separate document referencing the original intent_id |
| expired | No (none was promised) | — |
Receipts are verifiable by any third party at POST /api/atp/receipts/verify with no shared secret.
5. Retry policy (recommended)
- Exponential backoff, base 250 ms, jitter ±50 %, cap 30 s.
- Retry on
5xx,408,425,429(afterRetry-After),503, and on connection errors. - Do not retry on
4xxother than the above — the response is final. In particular:409 nonce_consumedand410 intent_expiredare terminal. - Bound total retry budget to the intent's
expires_at; never retry past expiry.
6. Network-partition behavior
Failure modes during a partition:
- Agent ⇄ site network loss after authorize, before execute response.
The agent re-issues
POST /executewith the same idempotency key when connectivity returns. If the original request committed, the agent gets the existing terminal record. If it didn't, it executes once. - Site ⇄ payment-rail loss after charging.
The intent sits in
executinguntil the rail callback or a reconciliation sweep completes. Agents observeexecutingon polling and back off; they do not retry the intent itself. - Reconciliation.
A periodic job (default 60 s) reconciles
executingrows against the rail's idempotent reference (e.g. Stripe PaymentIntent ID). Outcomes resolve tosettledorcompensated. - Split-brain replicas.
ATP assumes a single authoritative writer per
(intent_id, idempotency_key). Multi-region operators must route to the same primary or use a strong-consistency store. Eventual-consistency stores break the exactly-once guarantee — they are explicitly unsupported.
7. Duplicate-intent detection
Distinct from duplicate requests: a duplicate intent is when the agent generates two independent intent_ids for what the user meant as one action (the classic "double-click checkout" failure). WAB does not eliminate this — it's a UX problem — but it limits the blast radius:
- Recommended: agents include an
action_dedup_keyin the signed intent (e.g. hash of cart contents + user_id + minute bucket). Sites that opt in reject duplicateaction_dedup_keywithin the configured window. - Always: the intent
nonceis single-use, so an agent that replays the same signed intent gets a deterministic409 nonce_consumed.
8. Ordering
Intents are independent. ATP does not impose a total order between two intents from the
same agent or user. If your business logic requires "A before B", encode that as a precondition inside
the intent body (e.g. "execute only if intent X is settled"). The server evaluates the
precondition at authorize time.
9. Concrete examples
Successful retry after timeout
POST /api/atp/execute
Idempotency-Key: 9b1c...
Body: { intent_id: "int_abc", ... }
→ (timeout, agent retries the same request)
POST /api/atp/execute
Idempotency-Key: 9b1c... ← same key
Body: { intent_id: "int_abc", ... } ← same body
→ 200 OK
{ "state": "settled", "receipt": { ...signed... } }
(same row, no double-execution)
Nonce reuse (rejected)
POST /api/atp/authorize
Body: { intent_id: "int_abc", nonce: "n1", signature: "..." }
→ 200 authorized
POST /api/atp/authorize
Body: { intent_id: "int_abc", nonce: "n1", signature: "..." } ← same nonce
→ 409 { "error": "nonce_consumed", "first_consumed_at": "..." }
10. Document history
- 2026-05-25 — Initial publication.
Related: ATP overview · Security · Threat model · Benchmarks