PaymentOperator
The main payment operator contract with pluggable conditions for flexible authorization logic.Overview
- Type: Operator instance (one per fee recipient + configuration)
- Deployment: Via PaymentOperatorFactory
- Immutability: Cannot be paused or upgraded
- Configuration: 10 slots for conditions and recorders
- Use Cases: Marketplace, subscriptions, streaming, grants, custom flows
Immutable Fields
State
10-Slot Configuration
Condition Slots (Before Actions)
Condition Slots (Before Actions)
- AUTHORIZE_CONDITION - Who can authorize payments
- CHARGE_CONDITION - Who can charge partial amounts
- RELEASE_CONDITION - Who can release from escrow
- REFUND_IN_ESCROW_CONDITION - Who can refund during escrow
- REFUND_POST_ESCROW_CONDITION - Who can refund after release
address(0) = always allowRecorder Slots (After Actions)
Recorder Slots (After Actions)
- AUTHORIZE_RECORDER - Record authorization (e.g., timestamp)
- CHARGE_RECORDER - Record charge event
- RELEASE_RECORDER - Record release
- REFUND_IN_ESCROW_RECORDER - Record in-escrow refund
- REFUND_POST_ESCROW_RECORDER - Record post-escrow refund
address(0) = no recording (no-op)Key Methods
authorize()
Authorizes a payment and locks funds in escrow.paymentInfo- Payment info struct (must haveoperator == address(this),feeReceiver == address(this))amount- Amount to authorizetokenCollector- Address of the token collector contractcollectorData- Data to pass to the token collector
- Check
AUTHORIZE_CONDITION(if set) - Validate fee bounds compatibility
- Store fees at authorization time (prevents protocol fee changes from breaking capture)
- Call
escrow.authorize() - Call
AUTHORIZE_RECORDER(if set) - Emit
AuthorizationCreated
AUTHORIZE_CONDITION (default: anyone)
charge()
Direct charge - collects payment and immediately transfers to receiver (no escrow hold).paymentInfo- Payment info struct (must haveoperator == address(this),feeReceiver == address(this))amount- Amount to chargetokenCollector- Address of the token collector contractcollectorData- Data to pass to the token collector
- Check
CHARGE_CONDITION(if set) - Validate fee bounds compatibility
- Call
escrow.charge()- funds go directly to receiver - Accumulate protocol fees for later distribution
- Call
CHARGE_RECORDER(if set) - Emit
ChargeExecuted
CHARGE_CONDITION (default: anyone)
Unlike
authorize(), funds go directly to receiver without escrow hold. Refunds are only possible via refundPostEscrow().release()
Releases funds from escrow to receiver (capture).paymentInfo- Payment info structamount- Amount to release
- Check
RELEASE_CONDITION(if set) - Use fees stored at authorization time
- Call
escrow.capture() - Accumulate protocol fees for later distribution
- Call
RELEASE_RECORDER(if set) - Emit
ReleaseExecuted
RELEASE_CONDITION
Marketplace example: Receiver OR StaticAddressCondition(arbiter) + escrow passed
Subscription example: StaticAddressCondition(serviceProvider)
DAO example: StaticAddressCondition(daoMultisig)
refundInEscrow()
Refunds payment while still in escrow (partial void).paymentInfo- Payment info structamount- Amount to return to payer
- Check
REFUND_IN_ESCROW_CONDITION(if set) - Call
escrow.partialVoid()to return funds to payer - Call
REFUND_IN_ESCROW_RECORDER(if set) - Emit
RefundInEscrowExecuted
REFUND_IN_ESCROW_CONDITION
Marketplace example: StaticAddressCondition(arbiter) - disputes
Return policy example: Receiver OR StaticAddressCondition(arbiter)
DAO example: StaticAddressCondition(daoMultisig)
Subscription example: address(0) - no refunds
refundPostEscrow()
Refunds payment after it has been released (captured).paymentInfo- Payment info structamount- Amount to refund to payertokenCollector- Address of the token collector that will source the refundcollectorData- Data to pass to the token collector (e.g., signatures)
- Check
REFUND_POST_ESCROW_CONDITION(if set) - Call
escrow.refund()- token collector enforces permission - Call
REFUND_POST_ESCROW_RECORDER(if set) - Emit
RefundPostEscrowExecuted
REFUND_POST_ESCROW_CONDITION. Permission is also enforced by the token collector (e.g., receiver must have approved it, or collectorData contains receiver’s signature).
Marketplace example: StaticAddressCondition(arbiter) - post-delivery disputes
Return policy example: Receiver - voluntary returns
Most configurations: address(0) - no post-escrow refunds
Fee System (Modular, Additive)
Fees are additive and modular:totalFee = protocolFee + operatorFee
Fee Architecture
- Protocol Fee: 3 bps (0.03%) = 0.30 USDC → goes to
protocolFeeRecipient - Operator Fee: 2 bps (0.02%) = 0.20 USDC → goes to
FEE_RECIPIENT - Total Fee: 5 bps (0.05%) = 0.50 USDC
- Receiver Gets: 999.50 USDC
authorize() time in authorizedFees[hash]. This prevents protocol fee changes from breaking capture of already-authorized payments.
- Arbiter (marketplace with disputes)
- Service Provider (subscriptions)
- Platform Treasury (platform-controlled)
- DAO Multisig (governance-controlled)
Fee Distribution
Fees accumulate in the operator contract and are distributed viadistributeFees(token):
Protocol Fee Changes (7-day Timelock)
Protocol fee calculator changes require a 7-day timelock onProtocolFeeConfig:
Security Features
- ReentrancyGuardTransient - EIP-1153 transient storage for gas-efficient reentrancy protection
- Ownership - Solady’s Ownable with 2-step transfer
- Timelock - 7-day delay on protocol fee changes (operator fees are immutable)
- Immutable Core - Escrow, conditions, and fee configuration cannot be changed
RefundRequest
Manages refund requests independent of operator implementation.Overview
- Type: Singleton (one per network)
- Deployment: Direct deployment (no factory)
- Purpose: Track refund request lifecycle
Request Types
- In-Escrow Refunds
- Post-Escrow Refunds
Who can request: Payer, Receiver, OR ArbiterTypical flow:
- Payer suspects fraud, requests refund
- Arbiter investigates
- Arbiter approves or denies request
- If approved, arbiter calls
operator.refundInEscrow()
- Buyer remorse
- Seller fraud
- Payment error
Request Status States
Key Methods
requestRefund()
Creates a new refund request.paymentInfo- Payment info structamount- Amount being requested for refundnonce- Record index (from PaymentIndexRecorder) identifying which charge/action
Each refund request is keyed by
(paymentInfoHash, nonce) where nonce is the record index. This allows multiple refund requests per payment (one per charge/action).updateStatus()
Approve or deny a refund request.paymentInfo- Payment info structnonce- Record index identifying which refund requestnewStatus- The new status (ApprovedorDenied)
REFUND_IN_ESCROW_CONDITION can also approve/deny.
Valid transitions:
Pending→ApprovedPending→Denied
cancelRefundRequest()
Payer cancels their own request.paymentInfo- Payment info structnonce- Record index identifying which refund request
Pending→Cancelled
Usage Example
RefundRequest is advisory only. Approval does not automatically execute refunds - the authorized party must call the operator’s refund function.
AuthCaptureEscrow
Base escrow contract from commerce-payments library.Overview
- Type: Singleton (one per network)
- Source: commerce-payments (x402r fork)
- Purpose: Hold ERC-20 tokens during payment lifecycle
- Access: Operator-based (only registered operators can manage payments)
x402r uses a forked version of commerce-payments with added partial void support for handling partially completed orders.
Payment State Machine
Key Methods
authorize()
Locks tokens in escrow. Called by operator.amount of token
The base escrow contract uses individual parameters (paymentId, payer, receiver, etc.) while the PaymentOperator wraps them in a
PaymentInfo struct. The operator translates between the two formats internally.release()
Releases tokens to receiver. Called by operator.InEscrow → Released
void()
Returns tokens to payer (full refund). Called by operator.InEscrow → Settled
Use case: Refund during escrow period
reclaim()
Takes tokens back from receiver to give to payer. Called by operator.Released → Settled
Use case: Refund after release
Requires: Receiver has approved escrow for amount
partialVoid()
Returns partial amount to payer.Security Features
- Operator whitelist - Only registered operators can manage payments
- Reentrancy protection - All state changes protected
- Event logging - Complete audit trail
Supporting Contracts
ERC3009PaymentCollector
Payment collection with gasless meta-transactions. Features:- ERC-3009
transferWithAuthorization()support - Nonce-based replay protection
- Deadline-based expiry
- Multicall3 integration
TokenStore
Safe token transfer utilities from commerce-payments. Features:- Reentrancy-safe transfers
- SafeERC20 integration
- Balance tracking
Contract Addresses
Base Sepolia Testnet
| Contract | Address |
|---|---|
| AuthCaptureEscrow | 0xb9488351E48b23D798f24e8174514F28B741Eb4f |
| ERC3009PaymentCollector | 0xed02d3E5167BCc9582D851885A89b050AB816a56 |
| RefundRequest | 0x6926c05193c714ED4bA3867Ee93d6816Fdc14128 |
| PaymentOperatorFactory | 0xFa8C4Cb156053b867Ae7489220A29b5939E3Df70 |
