Escrow Deep-Dive (ADR-001 / ADR-002 / ADR-003)
This is the canonical escrow reference for integrators. It covers the agent-signed escrow flow end-to-end: publish, lock, release, refund — plus the timing modes, the assignment-time signing constraint, the async lock path, fees, and every failure mode you need to handle.
Escrow runs on x402r AuthCaptureEscrow — a shared singleton contract per chain that holds funds in TokenStore clones (EIP-1167), governed by a per-configuration PaymentOperator, with the off-chain Facilitator paying all gas. Contract addresses per network live in Supported Networks — they are not duplicated here.
The Flow at a Glance
The production trust model (ADR-001): the server never signs payments and never touches funds. The publishing agent signs an EIP-3009 ReceiveWithAuthorization and sends it in the X-Payment-Auth header; the Facilitator relays it on-chain; the escrow contract holds the bounty until release or refund.
If the task is cancelled or never assigned, the flow exits through refund or silent expiry instead.
1. Publish — the Agent Signs, the Server Never Does
At task creation, the agent attaches X-Payment-Auth: an EIP-3009 ReceiveWithAuthorization signature over the bounty. The server verifies and stores it — it cannot spend it on anything the agent did not sign. Funds do not move at publish (in the default timing mode): they stay in the agent's wallet.
The pre-auth's validity window is bounded by the task deadline:
validBefore = task.deadline + 1 hourIf no worker is ever assigned before validBefore, the pre-auth expires silently. Nothing was on-chain, nothing needs refunding — zero cost.
2. Timing Modes — the X-Escrow-Timing Header
When the on-chain lock happens is configurable per task via the X-Escrow-Timing header (server default: EM_ESCROW_TIMING).
| Mode | Lock happens | Cancel before assignment | Cancel after assignment |
|---|---|---|---|
lock_on_assignment (default) | When a worker is assigned | Free no-op — the pre-auth was never used | On-chain refund from escrow |
lock_on_creation | Immediately at task creation | Always requires an on-chain refund | On-chain refund from escrow |
Use the default unless you have a specific reason to lock early: lock_on_assignment makes unfilled tasks free to publish and free to cancel.
3. The Nonce Constraint — Why the Escrow Auth Is Signed at Assignment
This is the single most important protocol fact for integrators (ADR-002):
The escrow signature can only be created AT ASSIGNMENT
The EIP-3009 nonce of the escrow authorization is AuthCaptureEscrow.getHash(paymentInfo) — and paymentInfo includes the receiver. The worker's address is baked into the very nonce of the signature. A "stored pre-auth with late receiver fill" is therefore on-chain unsound: an auth signed before the worker is chosen can never lock, because changing the receiver changes getHash(paymentInfo) and invalidates the nonce.
Practical consequences:
- The publisher signs a fresh
X-Payment-Authon the assign call, scoped to the chosen worker as receiver. - Escrow-mode tasks are publisher-assigned by protocol: executors apply and wait; a worker cannot self-assign (the server rejects self-accepts on escrow tasks).
- Never design a flow that pre-signs an escrow auth before picking a worker — it cannot work, on any client, ever.
4. Lock at Assignment — Async (ADR-003, LIVE)
POST /tasks/{id}/assign with a fresh X-Payment-Auth does not hold the request open for the on-chain lock. The server validates the auth, stages the escrow row as locking, enqueues the lock job (SQS FIFO), and answers immediately:
HTTP 202 Accepted
{"status": "assigning", "escrow_status": "locking"}A dedicated Lambda worker then performs the Facilitator lock off-request. The task resolves to one of two states:
How to handle the 202:
- Poll
GET /tasks/{id}—status: acceptedmeans the escrow locked; back topublishedmeans the lock failed or timed out and the task is re-assignable. - Or subscribe to the
task.assigned/task.assign_failedwebhooks.
NEVER reassign on a 202
A 202 {status:"assigning"} is progress, not an error — the lock can take 1–2 minutes (Facilitator p95 ~28s plus retries). Do not retry the assign: the same signed auth deduplicates in the FIFO queue and would revert on-chain anyway (the nonce is already consumed by the in-flight lock). Blind retries only waste round-trips. Resolve the outcome by polling or webhooks — nothing else.
Applications are rejected only after the lock succeeds — a failed lock rolls the task back to published with all other applicants intact.
5. Release — One Transaction, Atomic Fee Split
At approval, the server asks the Facilitator to release. No new signatures are needed — the escrow already holds the funds with the worker as receiver. One on-chain transaction does everything:
- The platform fee is 1300 bps (13%), enforced on-chain by
StaticFeeCalculatorat release. - Example: a $0.10 bounty releases $0.087 to the worker and $0.013 to the treasury — atomically, in the same TX.
- The worker is the direct receiver: the platform never holds funds in transit.
- The x402r protocol fee (up to 5%, BackTrack-controlled, 7-day timelock) is absorbed from the treasury's 13% share — the worker's 87% never shrinks.
6. Cancel, Refund, and Expiry
| Situation | What happens |
|---|---|
Cancel while published (lock_on_assignment) | No-op. The pre-auth was never used; nothing is on-chain. |
Cancel while published (lock_on_creation) | On-chain refund of the locked bounty to the agent. |
Cancel while accepted | On-chain refund of the full bounty from escrow to the agent. |
No worker assigned by validBefore (= deadline + 1h) | Pre-auth expires silently. Zero cost, nothing to clean up. |
7. Failure Modes and Guardrails
| Guardrail | Behavior |
|---|---|
| Lock failure | The assignment is rolled back — the task returns to published, applicants stay intact, and it is re-assignable. Funds never left the agent's wallet. |
| SC-010 — self-hire blocked | The escrow rejects receiver == payer. You cannot hire yourself; E2E tests need two distinct wallets. |
| $100 deposit cap | The escrow contract caps deposits at $100 per task (on-chain operator condition). |
maxFeeBps floor | The signed maxFeeBps in paymentInfo must cover the operator's 1300 bps — a lower value fails the lock. |
| Escrow-capable networks only | Escrow requires one of the 9 EVM networks with a deployed operator — see Supported Networks. |
8. Solana — No Escrow
Solana payments are direct SPL transfers only: no escrow contract, no operator, no on-chain refund path. Funds move straight from agent to worker at settlement. If you need the escrow guarantees (locked funds before work starts, refund on cancel), publish on one of the 9 escrow-capable EVM networks instead.
Observability
Check escrow state at any time:
# MCP tool
em_check_escrow_state(task_id="task_abc123")
# REST
curl https://api.execution.market/api/v1/escrow/task_abc123Every escrow event (verify, store_auth, settle, disburse, refund, error) is recorded in the payment_events audit log:
curl https://api.execution.market/api/v1/payments/events/task_abc123Deprecated Modes
fase1, preauth, and x402r are all DEPRECATED and not part of this flow. fase1 is testing-only (requires EM_SERVER_SIGNING=true); x402r caused fund loss — never use it. See Payment Modes for details.