System Overview
For more visual diagrams, see the x402r-contracts repository.Payment Flow Sequence
Standard Payment (Happy Path)
- Payer calls
operator.authorize(paymentInfo, amount, tokenCollector, collectorData) - Operator checks
AUTHORIZE_PRE_ACTION_CONDITION(if set) - Operator validates fee bounds and stores fees at authorization time
- Operator calls
escrow.authorize()to lock funds - Operator calls
AUTHORIZE_POST_ACTION_HOOKto record timestamp - Escrow period begins (for example, 7 days) if configured
- After escrow period: Authorized addresses call
operator.capture(paymentInfo, amount)(for example, receiver, designated address, or both) - Operator checks
CAPTURE_PRE_ACTION_CONDITION(configurable, can include time checks or role checks) - Operator calls
escrow.capture()to transfer funds to receiver - Operator accumulates protocol fees for later distribution
- Operator calls
CAPTURE_POST_ACTION_HOOKto update state
Void Flow (before capture)
Example: Marketplace with arbiter dispute resolution- Payer calls
refundRequest.requestRefund(paymentInfo, amount) - RefundRequest creates request with status
Pending - Designated address (for example, arbiter or DAO multisig) reviews dispute
- Designated address calls
operator.void(paymentInfo) - Operator checks
VOID_PRE_ACTION_CONDITION(configured per operator) - Operator calls
escrow.void()to return all escrowed funds to payer - Operator calls
VOID_POST_ACTION_HOOK(RefundRequest flips status toApproved) - 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.
Condition Evaluation Flow
Authorization Check (Before Action)
When you invoke an action (for example,capture()):
- Load Condition - Get the condition address from operator slot
- 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)
- Result:
true→ Proceed to execute actionfalse→ Revert withPreActionConditionNotMeterror
- Execute Action - Call escrow method
- Call Hook - Run the matching
*_POST_ACTION_HOOKafter 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
- 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
| Role | Capabilities | Restrictions |
|---|---|---|
| Payer | authorize(), freeze(), unfreeze(), requestRefund(), cancelRefundRequest() | Can only act on own payments |
| Receiver | capture() (if condition allows), charge(), requestRefund() | Can only act on payments where they are receiver |
| Designated Address | Any action per conditions (for example, void(), capture(), or refund()) | Defined by StaticAddressCondition (arbiter, DAO, or service provider) |
| Protocol Owner | queueCalculator(), executeCalculator(), queueRecipient(), executeRecipient() | 7-day timelock on ProtocolFeeConfig changes |
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 useReentrancyGuardTransient (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 onProtocolFeeConfig (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:- Current owner calls
requestOwnershipHandover(newOwner) - New owner calls
completeOwnershipHandover() - 48-hour window for completion
Event Architecture
Core Events
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.
