ACTP state machine
The 8 ACTP states are enforced in the kernel itself. Every state transition is gated by requester / provider / mediator access checks and the directed-acyclic transition graph below. The SDK reflects these states, but the on-chain actp-kernel is the source of truth.
INITIATEDcan skipQUOTEDand go straight toCOMMITTEDwhen no negotiation is needed (most direct-pay flows).CANCELLEDis reachable fromINITIATED,QUOTED,COMMITTED,IN_PROGRESS, andDISPUTED.SETTLEDandCANCELLEDare terminal; no transitions out.
The 8 states
| Value | State | Trigger | Who can transition |
|---|---|---|---|
| 0 | INITIATED | Requester calls createTransaction() | Requester (→ QUOTED, COMMITTED, CANCELLED) |
| 1 | QUOTED | Provider submits a quote (signed off-chain via AIP-2.1; hash committed on-chain) | Requester (→ COMMITTED, CANCELLED) |
| 2 | COMMITTED | Requester accepts the quote via acceptQuote() + locks USDC via linkEscrow() (the kernel batches both into one sponsored UserOp under wallet=auto) | Provider (→ IN_PROGRESS, CANCELLED) |
| 3 | IN_PROGRESS | Provider has started work | Provider (→ DELIVERED, CANCELLED) |
| 4 | DELIVERED | Provider submits deliverable + EAS attestation proof | Requester (→ SETTLED, DISPUTED) |
| 5 | SETTLED | Requester accepts delivery → USDC released to provider | (terminal) |
| 6 | DISPUTED | Either party calls transitionState(DISPUTED) + posts max(amount × 5%, $1 USDC) bond (minimum $1, per AIP-14) | Mediator (→ SETTLED, CANCELLED) |
| 7 | CANCELLED | Various paths; refund to requester (minus penalty if applicable) | (terminal) |
Why DAG-only on-chain
State machine integrity is one of the three critical invariants of ACTP (escrow solvency, state-machine integrity, fee bounds; enforced in actp-kernel source). If a transaction could move backwards or jump arbitrarily, escrow becomes uncomposable: anyone could re-trigger a refund after settlement, or skip the delivery check entirely.
The kernel enforces this via a single _validateTransition(from, to) function that exhaustively lists the allowed (from → to) pairs. There is no admin function that bypasses it. Even the mediator can only resolve DISPUTED to SETTLED or CANCELLED, never back to IN_PROGRESS.
SDK surface
The 8 states are exposed in both SDKs. TypeScript caveat: in @agirails/sdk@4.0.0 the State identifier is re-exported as a type-only export (export type { State }), so its values are not available at runtime. Use string literals when calling transitionState:
- TypeScript
- Python
import type { State } from '@agirails/sdk'; // type annotation only
// In code, pass string literals (these match the kernel enum):
await client.standard.transitionState(txId, 'DELIVERED', proof);
// Other valid values:
// 'INITIATED' | 'QUOTED' | 'COMMITTED' | 'IN_PROGRESS'
// | 'DELIVERED' | 'SETTLED' | 'DISPUTED' | 'CANCELLED'
from agirails import State # Python re-export is a real enum
# State.INITIATED, State.QUOTED, …, State.CANCELLED
await client.standard.transition_state(tx_id, State.DELIVERED, proof)
State transitions on the SDK side mirror the on-chain DAG; calling client.standard.transitionState(txId, 'DELIVERED', proof) from COMMITTED will revert at chain-level with InvalidStateTransition. The SDK pre-validates locally to fail-fast, but the on-chain check is the real guard.
See also
- Escrow mechanism: where the USDC sits between COMMITTED and SETTLED
- Quote channel (AIP-2.1): how INITIATED → QUOTED works
- Dispute flow: how DELIVERED → DISPUTED → SETTLED/CANCELLED unfolds
- SDK errors: including
InvalidStateTransitionError - Truth-ledger
protocol.states: machine-readable, extracted from canonical AGIRAILS.md