Skip to main content

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.

ACTP state machine: 8 states with terminal SETTLED + CANCELLED, dispute branch from DELIVERED
  • INITIATED can skip QUOTED and go straight to COMMITTED when no negotiation is needed (most direct-pay flows).
  • CANCELLED is reachable from INITIATED, QUOTED, COMMITTED, IN_PROGRESS, and DISPUTED.
  • SETTLED and CANCELLED are terminal; no transitions out.

The 8 states

ValueStateTriggerWho can transition
0INITIATEDRequester calls createTransaction()Requester (→ QUOTED, COMMITTED, CANCELLED)
1QUOTEDProvider submits a quote (signed off-chain via AIP-2.1; hash committed on-chain)Requester (→ COMMITTED, CANCELLED)
2COMMITTEDRequester accepts the quote via acceptQuote() + locks USDC via linkEscrow() (the kernel batches both into one sponsored UserOp under wallet=auto)Provider (→ IN_PROGRESS, CANCELLED)
3IN_PROGRESSProvider has started workProvider (→ DELIVERED, CANCELLED)
4DELIVEREDProvider submits deliverable + EAS attestation proofRequester (→ SETTLED, DISPUTED)
5SETTLEDRequester accepts delivery → USDC released to provider(terminal)
6DISPUTEDEither party calls transitionState(DISPUTED) + posts max(amount × 5%, $1 USDC) bond (minimum $1, per AIP-14)Mediator (→ SETTLED, CANCELLED)
7CANCELLEDVarious paths; refund to requester (minus penalty if applicable)(terminal)
Transaction lifecycle: full path from INITIATED through QUOTED/COMMITTED/IN_PROGRESS/DELIVERED to terminal SETTLED, with CANCELLED + DISPUTED branches

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:

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'

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