System Overview
For additional 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_CONDITION(if set) - Operator validates fee bounds and stores fees at authorization time
- Operator calls
escrow.authorize()to lock funds - Operator calls
AUTHORIZE_RECORDERto record timestamp - Escrow period begins (e.g., 7 days) - if configured
- After escrow period: Authorized address(es) call
operator.release(paymentInfo, amount)(e.g., receiver, designated address, or both) - Operator checks
RELEASE_CONDITION(configurable - can include time checks, role checks, etc.) - Operator calls
escrow.capture()to transfer funds to receiver - Operator accumulates protocol fees for later distribution
- Operator calls
RELEASE_RECORDERto update state
Refund Flow (In-Escrow)
Example: Marketplace with arbiter dispute resolution- Payer calls
refundRequest.requestRefund(paymentInfo, amount, nonce) - RefundRequest creates request with status
Pending - Designated address (e.g., arbiter, DAO multisig) reviews dispute
- Designated address calls
refundRequest.updateStatus(paymentInfo, nonce, Approved) - Designated address calls
operator.refundInEscrow(paymentInfo, amount) - Operator checks
REFUND_IN_ESCROW_CONDITION(configured per operator) - Operator calls
escrow.partialVoid()to return funds to payer - Operator calls
REFUND_IN_ESCROW_RECORDER - 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, release blocked
- Day 6: Freeze expires automatically (or authorized address unfreezes early)
- Day 7: Escrow period ends
- Day 7+: Authorized address(es) can release (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 an action is called (e.g.,release()):
- Load Condition - Get the condition address from operator slot
- Evaluate Condition - Call
condition.check(paymentInfo, amount, caller)- Check if caller matches required role (e.g., receiver, arbiter)
- Check state (e.g., escrow period passed, not frozen)
- Check other requirements (e.g., time constraints)
- Result:
true→ Proceed to execute actionfalse→ Revert withConditionNotMeterror
- Execute Action - Call escrow method
- Call Recorder - Update state 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
- 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
For a 1000 USDC payment with 3 bps protocol fee + 2 bps operator fee:
- Protocol Fee: 0.30 USDC (3 bps) →
protocolFeeRecipienton ProtocolFeeConfig - Operator Fee: 0.20 USDC (2 bps) →
FEE_RECIPIENTon operator - Total Fee: 0.50 USDC (5 bps)
- Receiver Gets: 999.50 USDC
distributeFees(token).
FEE_RECIPIENT 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 | release() (if condition allows), charge(), requestRefund() | Can only act on payments where they are receiver |
| Designated Address | Any action per conditions (e.g., refundInEscrow(), release(), updateStatus()) | Defined by StaticAddressCondition (arbiter, DAO, service provider, etc.) |
| Protocol Owner | queueCalculator(), executeCalculator(), queueRecipient(), executeRecipient() | 7-day timelock on ProtocolFeeConfig changes |
“Designated Address” is configured per operator via StaticAddressCondition. Can be:
- 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 changes require 7-day delay onProtocolFeeConfig:
Operator fees are immutable - set at deploy time via
IFeeCalculator. Only protocol fees can be changed (with 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
