Skip to main content

System Overview

For more visual diagrams, see the x402r-contracts repository.

Payment Flow Sequence

Standard Payment (Happy Path)

  1. Payer calls operator.authorize(paymentInfo, amount, tokenCollector, collectorData)
  2. Operator checks AUTHORIZE_PRE_ACTION_CONDITION (if set)
  3. Operator validates fee bounds and stores fees at authorization time
  4. Operator calls escrow.authorize() to lock funds
  5. Operator calls AUTHORIZE_POST_ACTION_HOOK to record timestamp
  6. Escrow period begins (for example, 7 days) if configured
  7. After escrow period: Authorized addresses call operator.capture(paymentInfo, amount) (for example, receiver, designated address, or both)
  8. Operator checks CAPTURE_PRE_ACTION_CONDITION (configurable, can include time checks or role checks)
  9. Operator calls escrow.capture() to transfer funds to receiver
  10. Operator accumulates protocol fees for later distribution
  11. Operator calls CAPTURE_POST_ACTION_HOOK to update state

Void Flow (before capture)

Example: Marketplace with arbiter dispute resolution
  1. Payer calls refundRequest.requestRefund(paymentInfo, amount)
  2. RefundRequest creates request with status Pending
  3. Designated address (for example, arbiter or DAO multisig) reviews dispute
  4. Designated address calls operator.void(paymentInfo)
  5. Operator checks VOID_PRE_ACTION_CONDITION (configured per operator)
  6. Operator calls escrow.void() to return all escrowed funds to payer
  7. Operator calls VOID_POST_ACTION_HOOK (RefundRequest flips status to Approved)
  8. Funds transferred back to payer
Refund conditions are configurable. Can be arbiter-only (marketplace), receiver-allowed (return policy), DAO-controlled (governance), or disabled (subscriptions).

Freeze Flow

Example: Marketplace with payer freeze protection Timeline:
  • Day 0: Payment authorized, escrow period begins
  • Day 0-7: Payer can freeze if suspicious (per Freeze contract configuration)
  • Day 3: Payer freezes payment (freeze lasts 3 days per configuration)
  • Day 3-6: Payment frozen, capture blocked
  • Day 6: Freeze expires automatically (or authorized address unfreezes early)
  • Day 7: Escrow period ends
  • Day 7+: Authorized addresses can capture (if not frozen)
Freeze policies are optional and configurable. Define who can freeze, who can unfreeze, and how long freeze lasts.
MEV Protection: Payers should freeze EARLY if suspicious, not at the deadline. Use private mempool (Flashbots Protect) if freezing near expiry to avoid front-running.

Condition Evaluation Flow

Authorization Check (Before Action)

When you invoke an action (for example, capture()):
  1. Load Condition - Get the condition address from operator slot
  2. Check Condition - Call condition.check(paymentInfo, amount, caller, data)
    • Check if caller matches required role (for example, receiver or arbiter)
    • Check state (for example, escrow period passed, not frozen)
    • Check other requirements (for example, time constraints)
  3. Result:
    • true → Proceed to execute action
    • false → Revert with PreActionConditionNotMet error
  4. Execute Action - Call escrow method
  5. Call Hook - Run the matching *_POST_ACTION_HOOK after successful execution

Combinator Example

OrCondition([ReceiverCondition, StaticAddressCondition(arbiter)])
  • Checks if caller is receiver: Yes → PASS
  • If not receiver, checks if caller is arbiter: Yes → PASS
  • If neither: FAIL
AndCondition([OrCondition, EscrowPeriod])
  • First checks OrCondition: PASS (caller is receiver or arbiter)
  • Then checks EscrowPeriod: PASS (escrow period elapsed)
  • Both passed → PASS (action allowed)
This example shows marketplace configuration. For subscriptions, you might use StaticAddressCondition(serviceProvider) instead. For DAO governance, use StaticAddressCondition(daoMultisig).

Data Flow

Payment Information

Operator State (Minimal)

EscrowPeriod Recording

Freeze State (Separate Contract)

Fee Distribution (Additive Model)

Fees are additive: totalFee = protocolFee + operatorFee. They are split between the protocol fee recipient (on ProtocolFeeConfig) and the operator’s FEE_RECEIVER. For a worked example with concrete amounts, see the Fee System. Fees accumulate in the operator. Anyone can call distributeFees(token) to disburse them. FEE_RECEIVER can be:
  • Arbiter address (marketplace with disputes)
  • Service provider address (subscriptions, APIs)
  • Platform treasury (platform-controlled)
  • DAO multisig (governance-controlled)

Roles & Permissions

Each operator sets its “Designated Address” via StaticAddressCondition. Common roles include:
  • Arbiter (marketplace with disputes)
  • Service Provider (subscriptions, APIs)
  • DAO Multisig (governance-controlled)
  • Platform Treasury (platform-controlled)
  • Compliance Officer (regulated services)

Security Features

Reentrancy Protection

All state-changing functions use ReentrancyGuardTransient (EIP-1153):
  • More gas-efficient than persistent storage
  • Automatic cleanup after transaction
  • Protection against cross-function reentrancy

Timelock Protection

Protocol fee calculator and recipient changes require a 7-day delay on ProtocolFeeConfig (queue, wait, execute). See Fee System: 7-day timelock for the full workflow with events and the cancel path.
Operator fees are immutable: set at deploy time via IFeeCalculator. Only protocol fees can change, and only after a 7-day timelock.

Two-Step Ownership

Ownership transfers use Solady’s Ownable pattern:
  1. Current owner calls requestOwnershipHandover(newOwner)
  2. New owner calls completeOwnershipHandover()
  3. 48-hour window for completion

Event Architecture

Core Events

These events enable off-chain monitoring and indexing.

Next Steps

PaymentOperator

Learn about the core operator contract.

Conditions

Explore the condition system and combinators.

Deploy an Operator

Deploy a PaymentOperator using the SDK.