# Architecture
Source: https://docs.x402r.org/contracts/architecture
System architecture, payment flows, and contract relationships
## System Overview
```mermaid theme={null}
flowchart TB
subgraph Users
Payer
Receiver
DesAddr["Designated Address
(Arbiter / Provider / DAO)"]
end
subgraph Factories
POF[PaymentOperatorFactory]
EPF[EscrowPeriodFactory]
FF[FreezeFactory]
end
subgraph Operator["PaymentOperator (per config)"]
Auth[authorize]
Charge[charge]
Capture[capture]
Void[void]
Refund[refund]
end
subgraph Plugins["Conditions & Hooks"]
Cond["ICondition
(check before action)"]
Hook["IHook
(run after action)"]
EP[EscrowPeriod]
Freeze[Freeze]
And[AndCondition]
Or[OrCondition]
RR[RefundRequest]
end
Escrow[AuthCaptureEscrow]
Payer -->|authorize / freeze| Operator
Receiver -->|capture / charge| Operator
DesAddr -->|void / refund / capture| Operator
Payer -->|requestRefund| RR
POF -->|deploys| Operator
EPF -->|deploys| EP
FF -->|deploys| Freeze
Operator -->|checks| Cond
Operator -->|calls| Hook
Operator -->|locks/captures funds| Escrow
EP -.->|implements| Cond
EP -.->|implements| Hook
Freeze -.->|implements| Cond
RR -.->|implements| Hook
And -.->|composes| Cond
Or -.->|composes| Cond
```
For more visual diagrams, see the [x402r-contracts repository](https://github.com/BackTrackCo/x402r-contracts#architecture).
## 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
```solidity theme={null}
// PaymentInfo is from base commerce-payments (AuthCaptureEscrow)
// Passed as calldata to operator methods - not stored in operator
struct PaymentInfo {
address operator; // The PaymentOperator address
address payer; // Client wallet
address receiver; // Fund recipient
address token; // ERC-20 token address
uint120 maxAmount; // Maximum authorized amount
uint48 preApprovalExpiry; // ERC-3009 validBefore / pre-approval deadline
uint48 authorizationExpiry;// Capture deadline (authorize path only)
uint48 refundExpiry; // Refund request deadline
uint16 minFeeBps; // Minimum fee in basis points
uint16 maxFeeBps; // Maximum fee in basis points
address feeReceiver; // Who receives fees (operator itself)
uint256 salt; // Client-provided entropy
}
```
### Operator State (Minimal)
```solidity theme={null}
// PaymentOperator stores only fee-related state
// Payment state is queried directly from escrow
// Fees locked at authorization time
mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees;
// Protocol fees pending distribution
mapping(address token => uint256) public accumulatedProtocolFees;
```
### EscrowPeriod Recording
```solidity theme={null}
// In EscrowPeriod (extends AuthorizationTimeRecorderHook)
mapping(bytes32 paymentInfoHash => uint256 authorizedAt) public authorizationTimes;
```
### Freeze State (Separate Contract)
```solidity theme={null}
// In Freeze contract
mapping(bytes32 paymentInfoHash => uint256 frozenUntil) public frozenUntil;
```
### 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](/contracts/fees#example-calculation).
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 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](/contracts/fees#calculator-changes-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
```solidity theme={null}
// Payment lifecycle (PaymentOperator events)
event AuthorizeExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount);
event ChargeExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount);
event CaptureExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount);
event VoidExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver);
event RefundExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount);
// Fee distribution
event FeesDistributed(address indexed token, uint256 protocolAmount, uint256 operatorAmount);
event OperatorDeployed(address indexed operator, address indexed deployer, address indexed feeReceiver);
// Freeze state (Freeze contract events)
event PaymentFrozen(bytes32 indexed paymentInfoHash, uint40 frozenAt);
event PaymentUnfrozen(bytes32 indexed paymentInfoHash);
```
These events enable off-chain monitoring and indexing.
## Next Steps
Learn about the core operator contract.
Explore the condition system and combinators.
Deploy a PaymentOperator using the SDK.
# Audits
Source: https://docs.x402r.org/contracts/audits
Audit status, trust assumptions, and security roadmap for x402r contracts
## Audit Status
x402r extends the canonical [commerce-payments](https://github.com/base/commerce-payments) protocol from Base. The commerce-payments contracts have professional audits and run directly at their universal CREATE2 addresses (no fork). The x402r-specific contracts on top of them are **not yet audited**.
### What's Audited (Upstream)
x402r runs the commerce-payments primitives at their canonical addresses, so their audit coverage applies directly with no fork to re-audit. Base maintains the authoritative, dated report list, defer to it rather than this page:
* [commerce-payments `audits/` directory](https://github.com/base/commerce-payments/tree/main/audits) hosts the report PDFs.
* [Security Audits section](https://github.com/base/commerce-payments#security-audits) of the upstream README lists each audit with its date and report link.
As of the latest published list, the `AuthCaptureEscrow` contract and its supporting infrastructure (TokenCollectors, TokenStore, Permit2 integration) were covered by five reports, three from Coinbase Protocol Security and two from Spearbit. These cover the core escrow lifecycle: `authorize`, `capture`, `void`, `reclaim`, and `refund`.
### What's Not Audited
| Component | Status | Risk |
| ---------------------- | ------------- | ------------------------------------------------------------------------------ |
| PaymentOperator | **Unaudited** | Core operator with condition/hook dispatch and fee system |
| PaymentOperatorFactory | **Unaudited** | CREATE2 deterministic deployment |
| ProtocolFeeConfig | **Unaudited** | Timelocked fee governance |
| StaticFeeCalculator | **Unaudited** | Simple immutable fee calculator |
| Condition plugins | **Unaudited** | PayerCondition, ReceiverCondition, StaticAddressCondition, AlwaysTrueCondition |
| Combinator plugins | **Unaudited** | AndCondition, OrCondition, NotCondition |
| EscrowPeriod | **Unaudited** | Combined hook + time-lock condition |
| Freeze | **Unaudited** | Freeze/unfreeze state management |
| Hook plugins | **Unaudited** | AuthorizationTimeRecorderHook, PaymentIndexRecorderHook, HookCombinator |
| RefundRequest | **Unaudited** | Refund request lifecycle management |
### What This Means
* The audited escrow layer covers fund custody, token transfers, and payment state transitions
* The condition/hook plugin system is stateless or minimal-state by design, reducing attack surface
Use x402r contracts on mainnet at your own risk. The x402r-specific code follows security best practices (CEI pattern, reentrancy guards, immutable configuration, timelocked governance), but has not undergone a formal audit.
## Security Practices
Even without a formal audit, the x402r contracts follow established security patterns:
* **CEI (Checks-Effects-Interactions)** ordering in all state-changing functions
* **ReentrancyGuardTransient** (EIP-1153) on all external entry points
* **Immutable configuration**: deployment locks the operator conditions and fee calculators
* **7-day timelock** on protocol fee changes via ProtocolFeeConfig
* **2-step ownership transfers** via Solady's Ownable
* **Forge test suite** covering core flows and edge cases
## Audit Roadmap
The plan is to pursue third-party audits as the contract architecture and use cases stabilize. Priority order:
1. **PaymentOperator**: condition dispatch, fee calculation, fee locking, distribution
2. **Plugin system**: conditions, hooks, combinators, and their factories
3. **EscrowPeriod + Freeze**: time-lock enforcement and freeze state management
4. **RefundRequest**: request lifecycle and access control
Completed audit reports go public.
To discuss the security posture in more detail before integrating, or to report a vulnerability, reach out at [security@x402r.org](mailto:security@x402r.org).
# AlwaysTrueCondition
Source: https://docs.x402r.org/contracts/conditions/always-true
Allow anyone to call an action with no restrictions
## Overview
AlwaysTrueCondition allows anyone to call the action, no restrictions applied.
**Type:** Singleton, CREATE2 (deployed once, reused by all operators)
**Address (all supported chains):** `0x2ef2A6162aEF9Df1022ff51c011af94D99AB4904`
## Logic
```solidity theme={null}
function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata)
external pure returns (bool)
{
return true;
}
```
## When to Use
| Slot | Use Case |
| -------------------------------- | -------------------------------------------------------------- |
| `AUTHORIZE_PRE_ACTION_CONDITION` | Let anyone create payments (common for marketplace/e-commerce) |
**Use with caution for capture/refund slots.** Setting `CAPTURE_PRE_ACTION_CONDITION` or `VOID_PRE_ACTION_CONDITION` to AlwaysTrueCondition means anyone can capture or refund funds. This matches leaving the slot as `address(0)` (the default behavior), but makes the intent explicit.
## AlwaysTrueCondition vs `address(0)`
Both allow any caller, but there's a subtle difference:
| | `address(0)` | AlwaysTrueCondition |
| ------------ | ----------------------------------- | ------------------------------------ |
| **Behavior** | Skips condition check entirely | Calls `check()` which returns `true` |
| **Gas** | Slightly cheaper (no external call) | Minimal overhead (\~200 gas) |
| **Intent** | "No condition configured" | "Explicitly open to all" |
Use `address(0)` when you simply don't need a condition. Use AlwaysTrueCondition when you want to make the "open access" intent explicit in your configuration.
## Gas
**Cost:** Minimal, `pure` function returning a constant.
## Next Steps
Restrict to payer only.
See configurations using AlwaysTrueCondition.
# Combinators
Source: https://docs.x402r.org/contracts/conditions/combinators
Compose conditions with And, Or, and Not logical operators
## Overview
Combinator conditions compose two or more conditions with logical operators. Deploy each via its respective factory.
## AndCondition
All conditions must pass (`A && B && C`).
```typescript theme={null}
// Deploy via factory
const comboAddress = await andConditionFactory.write.deploy([
[RECEIVER_CONDITION, ESCROW_PERIOD_ADDRESS] // Must be receiver AND after escrow
]);
// Use in operator config
config.capturePreActionCondition = comboAddress;
```
**Example:** Capture requires receiver AND escrow period passed.
## OrCondition
At least one condition must pass (`A || B`).
```typescript theme={null}
// Receiver OR Arbiter can capture
const comboAddress = await orConditionFactory.write.deploy([
[RECEIVER_CONDITION, ARBITER_CONDITION]
]);
config.capturePreActionCondition = comboAddress;
```
**Example:** Either receiver or arbiter can capture.
## NotCondition
Inverts a condition (`!A`).
```typescript theme={null}
// Anyone EXCEPT payer can call
const comboAddress = await notConditionFactory.write.deploy([PAYER_CONDITION]);
config.capturePreActionCondition = comboAddress;
```
**Example:** Prevent payer from releasing their own payment.
## Nested Combinators
Combine combinators for complex logic:
```typescript theme={null}
// (Receiver OR Arbiter) AND EscrowPassed
const receiverOrArbiter = await orConditionFactory.write.deploy([
[RECEIVER_CONDITION, ARBITER_CONDITION]
]);
const capturePreActionCondition = await andConditionFactory.write.deploy([
[receiverOrArbiter, ESCROW_PERIOD_ADDRESS]
]);
config.capturePreActionCondition = capturePreActionConditionAddress;
```
**Logic Tree:**
```mermaid theme={null}
flowchart TD
AND[AndCondition] --> OR[OrCondition]
AND --> ESC[EscrowPeriod ✓]
OR --> REC[ReceiverCondition ✓]
OR --> ARB[ArbiterCondition ✗]
AND --> RES[Result: PASS]
```
(One branch of OR passed, AND both passed)
## Limits
**Max 10 conditions per combinator.** Keep combinators simple. Nested trees increase gas costs and make debugging harder.
## Gas
Simpler combinators = less gas:
```typescript theme={null}
// Better: 2 conditions
OrCondition([A, B]) // ~25K gas per check
// Worse: 4 conditions
OrCondition([A, B, C, D]) // ~45K gas per check
```
Each extra condition adds one external call. Prefer fewer conditions where possible.
## Next Steps
Time-based condition for escrow windows.
Block releases on frozen payments.
See combinators in real configurations.
Deploy combinators via factory contracts.
# Custom Conditions
Source: https://docs.x402r.org/contracts/conditions/custom
Build your own condition contracts for specialized authorization logic
## Overview
You can create custom conditions for specialized logic beyond what the built-in conditions provide. Build against the `ICondition` interface and follow the security rules below.
## ICondition Rules
From the `ICondition.sol` NatSpec:
1. **MUST NOT revert**: return `false` to deny, never `revert`
2. **Return `true` to allow, `false` to deny**
The operator converts a `false` return into a `PreActionConditionNotMet` revert. Prefer `view` or `pure` implementations to keep call sites cheap and gas predictable.
## Example: TimeOfDayCondition
A condition that only allows actions during specific hours (UTC):
```solidity theme={null}
contract TimeOfDayCondition is ICondition {
uint256 public immutable startHour; // e.g., 9 (9 AM)
uint256 public immutable endHour; // e.g., 17 (5 PM)
constructor(uint256 _startHour, uint256 _endHour) {
startHour = _startHour;
endHour = _endHour;
}
function check(
PaymentInfo calldata payment,
uint256,
address caller,
bytes calldata
) external view returns (bool) {
uint256 hour = (block.timestamp / 3600) % 24;
return hour >= startHour && hour < endHour;
}
}
```
Usage, deploy via a factory and use in operator config:
```typescript theme={null}
// Deploy via your custom factory
const businessHours = await timeOfDayConditionFactory.write.deploy([9, 17]);
// Use in operator config
config.capturePreActionCondition = businessHours;
```
## Security Checklist
Before deploying a custom condition:
* [ ] Returns `false` instead of reverting on denial
* [ ] Declared as `view` or `pure`
* [ ] No external calls to untrusted contracts
* [ ] No state modifications
* [ ] Handles edge cases (zero address, zero amount, uninitialized payments)
* [ ] Full test coverage with Forge tests
Custom conditions with bugs can lead to **permanently locked funds** (if `check()` always returns `false`) or **unauthorized access** (if `check()` always returns `true`). Test on Base Sepolia before mainnet deployment.
## Testing
Test custom conditions with Forge:
```solidity theme={null}
contract TimeOfDayConditionTest is Test {
TimeOfDayCondition condition;
function setUp() public {
condition = new TimeOfDayCondition(9, 17);
}
function test_allowsDuringBusinessHours() public {
// Set block.timestamp to 10 AM UTC
vm.warp(10 * 3600);
assertTrue(condition.check(paymentInfo, 0, caller, ""));
}
function test_deniesOutsideBusinessHours() public {
// Set block.timestamp to 8 PM UTC
vm.warp(20 * 3600);
assertFalse(condition.check(paymentInfo, 0, caller, ""));
}
}
```
## Next Steps
Review the condition system architecture.
Build custom state hooks.
# EscrowPeriod
Source: https://docs.x402r.org/contracts/conditions/escrow-period
Time-lock condition that records authorization time and enforces escrow windows
## Overview
EscrowPeriod is a dual-purpose contract, it functions as both a **hook** and a **condition**:
* **As a hook:** Records the `block.timestamp` at payment authorization
* **As a condition:** Returns `true` only after the escrow period has elapsed
Use the **same address** for both `AUTHORIZE_POST_ACTION_HOOK` and `CAPTURE_PRE_ACTION_CONDITION` slots on the operator.
**Type:** Per-deployment via [EscrowPeriodFactory](/contracts/factories)
## Architecture
```mermaid theme={null}
flowchart LR
EP[EscrowPeriod] -->|extends| ATR[AuthorizationTimeRecorderHook]
EP -->|implements| IC[ICondition]
ATR -->|implements| IR[IHook]
```
EscrowPeriod extends [AuthorizationTimeRecorderHook](/contracts/hooks/authorization-time) and adds `ICondition` implementation. You don't need to deploy AuthorizationTimeRecorderHook on its own, use EscrowPeriod directly.
## Logic
```solidity theme={null}
// ICondition, returns true when escrow period has passed
function check(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256,
address,
bytes calldata
) external view returns (bool allowed) {
return !isDuringEscrowPeriod(paymentInfo);
}
// View function to check if still in escrow period
function isDuringEscrowPeriod(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo
) public view returns (bool) {
bytes32 hash = escrow.getHash(paymentInfo);
uint256 authTime = authorizationTimes[hash];
if (authTime == 0) return false;
return block.timestamp < authTime + ESCROW_PERIOD;
}
```
**Checks:**
1. The payment has a recorded authorization timestamp
2. Current time >= authorization time + escrow period
## Deployment
Deploy via [EscrowPeriodFactory](/contracts/factories):
```typescript theme={null}
const escrowPeriodAddress = await escrowPeriodFactory.write.deploy([
7 * 24 * 60 * 60, // 7 days in seconds
zeroHash // bytes32(0) = operator-only
]);
```
Then configure the operator:
```typescript theme={null}
const config = {
authorizePostActionHook: escrowPeriodAddress, // Record auth time
capturePreActionCondition: escrowPeriodAddress, // Check escrow passed
// ...
};
```
## Composition with Freeze
Compose this condition with a separate [Freeze](/contracts/conditions/freeze) condition via [AndCondition](/contracts/conditions/combinators) to gate capture on both escrow elapsed **and** not frozen. See [Composition Patterns](/contracts/conditions/freeze#composition-patterns) for the wiring.
## Use Cases
* **Time-lock releases**: 7-day escrow for e-commerce
* **Delayed fund access**: Grace period before receiver can access funds
* **Buyer protection**: Give payers time to freeze or request refunds
## Gas
**Cost:** \~20k gas per `run()` call (one `SSTORE` for the timestamp). The `check()` call is a `view` function with one `SLOAD`.
## Next Steps
Add freeze protection to escrow periods.
Deploy EscrowPeriod via factory.
# Freeze
Source: https://docs.x402r.org/contracts/conditions/freeze
Block payment capture when frozen, with configurable freeze/unfreeze authorization
## Overview
Freeze is a standalone condition that blocks capture on frozen payments. It manages freeze and unfreeze state with configurable authorization and an optional duration-based auto expiry.
**Type:** Per-deployment via [FreezeFactory](/contracts/factories)
## Architecture
* Implements `ICondition`
* Freeze/unfreeze authorization via `ICondition` contracts (passed to constructor)
* Optionally linked to [EscrowPeriod](/contracts/conditions/escrow-period) to restrict freezing to during the escrow window
## Logic
```solidity theme={null}
// ICondition, returns false when frozen (blocks capture)
function check(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256,
address,
bytes calldata
) external view returns (bool allowed) {
return !isFrozen(paymentInfo);
}
```
## Deployment
Deploy via [FreezeFactory](/contracts/factories):
```typescript theme={null}
// Deploy Freeze with payer freeze, arbiter unfreeze, 3-day duration
const freeze = await freezeFactory.deploy(
PAYER_CONDITION, // freeze condition (payer protection)
ARBITER_CONDITION, // unfreeze condition (dispute resolution)
3 * 24 * 60 * 60, // 3 days (auto-expires, 0 = permanent)
escrowPeriod // optional: link to EscrowPeriod (address(0) = unconstrained)
);
```
## Composition Patterns
```solidity theme={null}
// Escrow period only: capturePreActionCondition = escrowPeriod
// Freeze only: capturePreActionCondition = freeze
// Both: capturePreActionCondition = AndCondition([escrowPeriod, freeze])
```
Use [AndCondition](/contracts/conditions/combinators) to require both escrow period elapsed **and** not frozen before capture.
## Freeze Duration
* Payment frozen at time `T`
* Freeze expires at `T + freezeDuration`
* After expiry, payment is automatically unfrozen
* Can be manually unfrozen earlier by the authorized party
* Duration of `0` means permanent freeze (until manually unfrozen)
| Duration | Use Case |
| -------- | --------------------------- |
| 1 day | Quick investigation period |
| 3 days | Standard fraud check window |
| 5-7 days | Extended investigation |
| 14+ days | Complex dispute resolution |
Freeze duration should balance payer protection with receiver UX. Too long and receivers may avoid the platform. Too short and payers can't adequately investigate.
## Use Cases
* **Buyer protection**: Payer freezes suspicious payments during escrow
* **Dispute holds**: Arbiter freezes payments pending investigation
* **Compliance**: Compliance officer freezes flagged transactions
## Gas
**Cost:** \~20k gas per freeze/unfreeze (one `SSTORE`). The `check()` call is a `view` with one `SLOAD`.
## Next Steps
Add time-based capture restrictions.
Deploy Freeze via FreezeFactory.
# Conditions Overview
Source: https://docs.x402r.org/contracts/conditions/overview
Pluggable condition system for flexible payment authorization and state tracking
## What conditions do
Conditions are swappable contracts that control who can perform actions on a PaymentOperator. Each operator has **5 condition slots**, one per action:
| Slot | Controls |
| -------------------------------- | --------------------------------- |
| `AUTHORIZE_PRE_ACTION_CONDITION` | Who can create payments |
| `CHARGE_PRE_ACTION_CONDITION` | Who can charge partial amounts |
| `CAPTURE_PRE_ACTION_CONDITION` | Who can capture funds from escrow |
| `VOID_PRE_ACTION_CONDITION` | Who can refund during escrow |
| `REFUND_PRE_ACTION_CONDITION` | Who can refund after capture |
These are the condition half of the operator's 10 slots. For the full slot layout alongside the post-action hooks, see [PaymentOperator: 10-slot configuration](/contracts/payment-operator#10-slot-configuration).
## ICondition Interface
```solidity theme={null}
interface ICondition {
function check(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 amount,
address caller,
bytes calldata data
) external view returns (bool allowed);
}
```
**Parameters:**
* `paymentInfo`, The payment information struct
* `amount`, The amount involved in the action (0 for authorization-only checks like refund request status updates)
* `caller`, The address attempting the action
* `data`, Arbitrary data forwarded from the caller (signatures, proofs, attestations)
**Return:** `true` if the caller can proceed, `false` otherwise.
## Default Behavior
**Condition slot = `address(0)`**: always returns `true` (allow). The action has no restrictions.
You only need to set conditions for slots you want to restrict. Leave the rest as `address(0)`.
## Singleton vs Per-Deployment
| Type | Examples | Deploy Strategy |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- |
| **Singleton** | [PayerCondition](/contracts/conditions/payer), [ReceiverCondition](/contracts/conditions/receiver), [AlwaysTrueCondition](/contracts/conditions/always-true) | Deployed once, reuse everywhere |
| **Per-deployment** | [StaticAddressCondition](/contracts/conditions/static-address), [EscrowPeriod](/contracts/conditions/escrow-period), [Freeze](/contracts/conditions/freeze) | Deploy per use case via [factories](/contracts/factories) |
| **Composable** | [And/Or/Not](/contracts/conditions/combinators) | Combine existing conditions via factories |
## Security Rules
**Conditions MUST NOT revert.** Return `false` to deny, never `revert`. The operator converts `false` into a `ConditionNotMet` error.
* Conditions should be `view` or `pure` to prevent reentrancy attacks
* Never make external state-changing calls inside a condition
* Cover edge cases in tests. Bugs in authorization logic can lock funds
## Configuration Patterns
Conditions compose to create flexible authorization policies. Here are common patterns:
### Open Authorization, Restricted Capture
```solidity theme={null}
config = {
authorizePreActionCondition: ALWAYS_TRUE_CONDITION, // Anyone can authorize
authorizePostActionHook: escrowHook, // Record time
capturePreActionCondition: capturePreActionCondition, // Restricted
// ...
};
```
### Payer-Only Actions
```solidity theme={null}
config = {
authorizePreActionCondition: PAYER_CONDITION, // Only payer
capturePreActionCondition: PAYER_CONDITION, // Only payer
// ...
};
```
### Arbiter-Controlled
```solidity theme={null}
config = {
authorizePreActionCondition: ARBITER_CONDITION, // Only arbiter
chargePreActionCondition: ARBITER_CONDITION, // Only arbiter
capturePreActionCondition: ARBITER_CONDITION, // Only arbiter
voidPreActionCondition: ARBITER_CONDITION, // Only arbiter
refundPreActionCondition: ARBITER_CONDITION,
// ...
};
```
For complete configuration examples, see the [Examples](/contracts/examples) page.
## Gas Optimization
### Singleton Reuse
All operators reuse the same singleton conditions. Reference the existing addresses, don't deploy new instances:
```typescript theme={null}
// Good: Reference the singleton address
const config1 = { authorizePreActionCondition: PAYER_CONDITION };
const config2 = { authorizePreActionCondition: PAYER_CONDITION }; // Same address
```
### Stateless Conditions
Prefer stateless conditions when possible:
```solidity theme={null}
// Stateless: No storage reads (pure)
function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata)
external pure returns (bool)
{
return caller == payment.receiver; // Pure computation
}
// Stateful: Storage reads cost gas (view)
function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata)
external view returns (bool)
{
return allowList[caller]; // SLOAD costs gas
}
```
## Next Steps
Learn about the state recording system.
Compose conditions with And/Or/Not logic.
Build your own condition contracts.
See complete configuration examples.
# PayerCondition
Source: https://docs.x402r.org/contracts/conditions/payer
Singleton condition that restricts operator actions to the payment payer address
## Overview
PayerCondition is a singleton condition that restricts an action to the payment's payer address.
**Type:** Singleton, CREATE2 (deployed once, reused by all operators)
**Address (all supported chains):** `0x586486394C38A2a7d36B16a3FDaF366cd202d823`
## Logic
```solidity theme={null}
function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata)
external pure returns (bool)
{
return caller == payment.payer;
}
```
The condition compares `caller` against `payment.payer`, pure computation with no storage reads.
## When to Use
| Slot | Use Case |
| -------------------------------- | --------------------------------------------------- |
| `AUTHORIZE_PRE_ACTION_CONDITION` | Let payer create payments (subscriptions, invoices) |
| `VOID_PRE_ACTION_CONDITION` | Let payer request refunds during escrow |
| `REFUND_PRE_ACTION_CONDITION` | Let payer cancel streams |
Typically paired with [ReceiverCondition](/contracts/conditions/receiver) for capture, since payers shouldn't capture their own funds in most configurations.
## Gas
**Cost:** Minimal, `pure` function with no storage reads.
## Next Steps
Restrict actions to the payment receiver.
Combine PayerCondition with other conditions.
# ReceiverCondition
Source: https://docs.x402r.org/contracts/conditions/receiver
Singleton condition that restricts operator actions to the payment receiver address
## Overview
ReceiverCondition is a singleton condition that restricts an action to the payment's receiver address.
**Type:** Singleton, CREATE2 (deployed once, reused by all operators)
**Address (all supported chains):** `0x321651df4593DA57C413579c5b611D1A90168a3A`
## Logic
```solidity theme={null}
function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata)
external pure returns (bool)
{
return caller == payment.receiver;
}
```
The condition compares `caller` against `payment.receiver`, pure computation with no storage reads.
## When to Use
| Slot | Use Case |
| ------------------------------ | ---------------------------------------------- |
| `CAPTURE_PRE_ACTION_CONDITION` | Let receiver capture funds after escrow |
| `CHARGE_PRE_ACTION_CONDITION` | Let receiver charge partial amounts |
| `VOID_PRE_ACTION_CONDITION` | Let receiver issue refunds at their discretion |
For capture, ReceiverCondition is often composed with [EscrowPeriod](/contracts/conditions/escrow-period) via [AndCondition](/contracts/conditions/combinators) to ensure the escrow window has passed before the receiver can capture.
## Gas
**Cost:** Minimal, `pure` function with no storage reads.
## Next Steps
Restrict actions to the payment payer.
Add time-based capture restrictions.
# StaticAddressCondition
Source: https://docs.x402r.org/contracts/conditions/static-address
Restrict operator actions to a specific designated address via immutable check
## Overview
StaticAddressCondition restricts an action to a single designated address. Unlike PayerCondition and ReceiverCondition (which read from payment data), this condition checks against an immutable address set at deployment.
**Type:** Per-deployment (deploy one per designated address via [factory](/contracts/factories))
## Logic
```solidity theme={null}
contract StaticAddressCondition is ICondition {
address public immutable DESIGNATED_ADDRESS;
constructor(address _designatedAddress) {
DESIGNATED_ADDRESS = _designatedAddress;
}
function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata)
external view returns (bool)
{
return caller == DESIGNATED_ADDRESS;
}
}
```
## When to Use
| Role | Description |
| ---------------------- | ---------------------------------------------------- |
| **Arbiter** | Deploy with arbiter address for dispute resolution |
| **Service Provider** | Deploy with provider address for subscriptions |
| **DAO Treasury** | Deploy with multisig address for governance |
| **Compliance Officer** | Deploy with compliance address for approvals |
| **Platform** | Deploy with platform address for controlled releases |
## Example
Deploy via [StaticAddressConditionFactory](/contracts/factories):
```typescript theme={null}
// For marketplace arbiter
const arbiterCondition = await staticAddressConditionFactory.write.deploy([arbiterAddress]);
// For subscription service provider
const providerCondition = await staticAddressConditionFactory.write.deploy([serviceProviderAddress]);
// For DAO governance, same address produces the same deterministic deployment (idempotent)
const daoCondition = await staticAddressConditionFactory.write.deploy([daoMultisigAddress]);
```
## Gas
**Cost:** Minimal. `view` function with a single `immutable` read (compiled as a constant in bytecode, not a storage read).
## Next Steps
Deploy StaticAddressCondition via factory.
Compose with other conditions using And/Or/Not.
# Configuration Examples
Source: https://docs.x402r.org/contracts/examples
Complete configuration examples for common x402r use cases
The configuration examples below use simplified pseudo-code (for example, `new StaticAddressCondition(args)` or `new AndCondition([list])`) to illustrate how conditions compose. In practice, deploy conditions via their respective [factory contracts](/contracts/factories) using viem. See the [Deploy an operator guide](/sdk/deploy-operator) for executable code.
## Example 1: Standard E-Commerce with 7-Day Escrow
**Use Case:** Online marketplace with buyer protection. 7-day escrow period, payer can freeze for 3 days, receiver or arbiter can capture after escrow.
### Complete Configuration
```typescript theme={null}
// Deploy designated address condition for arbiter
const arbiterCondition = await new StaticAddressCondition(arbiterAddress);
```
```typescript theme={null}
// 7-day escrow period (combined hook + condition)
const escrowPeriod = await escrowPeriodFactory.deploy(
7 * 24 * 60 * 60, // 7 days
zeroHash // bytes32(0) = operator-only
);
```
```typescript theme={null}
// Payer can freeze, arbiter can unfreeze (or wait for 3-day expiry),
// linked to the 7-day escrow period.
const freeze = await freezeFactory.deploy(
PAYER_CONDITION, // freezeCondition: only payer can freeze
arbiterCondition.address, // unfreezeCondition: only arbiter can unfreeze
3 * 24 * 60 * 60, // freezeDuration: 3 days (auto-expires; 0 = permanent)
escrowPeriod // escrowPeriodContract: restricts freeze() to the escrow window (address(0) = unconstrained)
);
```
```typescript theme={null}
// (Receiver OR Arbiter) AND (EscrowPassed AND NotFrozen)
const receiverOrArbiter = await new OrCondition([
RECEIVER_CONDITION,
arbiterCondition.address
]);
// Compose escrow period and freeze checks
const escrowAndFreeze = await new AndCondition([
escrowPeriod, // Escrow period passed
freeze // Not frozen
]);
const capturePreActionCondition = await new AndCondition([
receiverOrArbiter,
escrowAndFreeze
]);
```
```typescript theme={null}
const config = {
feeReceiver: arbiterAddress, // Arbiter earns fees for dispute resolution
feeCalculator: feeCalculatorAddress, // Operator fee calculator
authorizePreActionCondition: ALWAYS_TRUE_CONDITION,
authorizePostActionHook: escrowPeriod, // Same address for recording auth time
chargePreActionCondition: RECEIVER_CONDITION,
chargePostActionHook: '0x0000000000000000000000000000000000000000',
capturePreActionCondition: capturePreActionCondition,
capturePostActionHook: '0x0000000000000000000000000000000000000000',
voidPreActionCondition: arbiterCondition.address,
voidPostActionHook: '0x0000000000000000000000000000000000000000',
refundPreActionCondition: arbiterCondition.address,
refundPostActionHook: '0x0000000000000000000000000000000000000000'
};
const operator = await operatorFactory.deployOperator(config);
```
### Payment Flow
```mermaid theme={null}
sequenceDiagram
participant Buyer
participant Escrow
participant Operator
participant Seller
Note over Buyer,Seller: Day 0 - Authorization
Buyer->>Escrow: authorize(100 USDC)
Note over Escrow: Funds locked for 7 days
Note over Buyer,Seller: Day 5 - Buyer Detects Issue
Buyer->>Operator: freeze(paymentId)
Note over Operator: Payment frozen for 3 days
Note over Buyer,Seller: Day 7 - Escrow Period Ends
Note over Operator: Payment still frozen
Note over Buyer,Seller: Day 8 - Freeze Expires
Note over Operator: Freeze automatically expires
Note over Buyer,Seller: Day 9 - Capture
Seller->>Operator: capture(paymentInfo, amount)
Operator->>Escrow: capture funds
Escrow->>Seller: 99.95 USDC
Escrow->>Operator: 0.05 USDC
```
***
## Example 2: Instant Payment (Using Charge)
**Use Case:** Digital goods or services where the seller expects immediate payment.
### Configuration
```typescript theme={null}
// Deploy arbiter condition for disputes
const arbiterCondition = await new StaticAddressCondition(arbiterAddress);
const config = {
feeReceiver: arbiterAddress, // Arbiter earns fees
feeCalculator: feeCalculatorAddress, // Operator fee calculator
authorizePreActionCondition: '0x0000000000000000000000000000000000000000', // Not used (charge handles auth)
authorizePostActionHook: '0x0000000000000000000000000000000000000000',
chargePreActionCondition: RECEIVER_CONDITION, // Only receiver can charge
chargePostActionHook: '0x0000000000000000000000000000000000000000',
capturePreActionCondition: RECEIVER_CONDITION, // Fallback to capture remaining
capturePostActionHook: '0x0000000000000000000000000000000000000000',
voidPreActionCondition: arbiterCondition.address,
voidPostActionHook: '0x0000000000000000000000000000000000000000',
refundPreActionCondition: arbiterCondition.address,
refundPostActionHook: '0x0000000000000000000000000000000000000000'
};
const operator = await operatorFactory.deployOperator(config);
```
### Payment Flow
```
Buyer approves tokens → Seller calls charge() → Funds transferred in one tx
(authorizes + charges atomically)
```
**Trade-offs:**
* Single transaction, no separate `authorize` step
* Instant delivery for digital goods
* Better UX: seller gets paid immediately
* No buyer protection escrow period
* Payment moves to the captured state right away
***
## Example 3: Physical Goods with Extended Escrow
**Use Case:** International shipping with 14-day escrow and 5-day receiver freeze period.
### Configuration
```typescript theme={null}
// Deploy arbiter condition
const arbiterCondition = await new StaticAddressCondition(arbiterAddress);
// 14-day escrow (shipping + inspection)
const escrowPeriod = await escrowPeriodFactory.deploy(
14 * 24 * 60 * 60, // 14 days
zeroHash // bytes32(0) = operator-only
);
// Receiver freeze (product defect), arbiter unfreeze (dispute resolution),
// linked to the 14-day escrow period.
const freeze = await freezeFactory.deploy(
RECEIVER_CONDITION, // freezeCondition
arbiterCondition.address, // unfreezeCondition
5 * 24 * 60 * 60, // freezeDuration: 5 days
escrowPeriod // escrowPeriodContract
);
// Receiver OR Arbiter can capture (after escrow + not frozen)
const capturePreActionCondition = await new AndCondition([
await new OrCondition([RECEIVER_CONDITION, arbiterCondition.address]),
await new AndCondition([escrowPeriod, freeze])
]);
const config = {
feeReceiver: arbiterAddress, // Arbiter earns fees
feeCalculator: feeCalculatorAddress, // Operator fee calculator
authorizePreActionCondition: ALWAYS_TRUE_CONDITION,
authorizePostActionHook: escrowPeriod, // Same address for recording auth time
chargePreActionCondition: '0x0000000000000000000000000000000000000000',
chargePostActionHook: '0x0000000000000000000000000000000000000000',
capturePreActionCondition: capturePreActionCondition,
capturePostActionHook: '0x0000000000000000000000000000000000000000',
voidPreActionCondition: arbiterCondition.address,
voidPostActionHook: '0x0000000000000000000000000000000000000000',
refundPreActionCondition: arbiterCondition.address,
refundPostActionHook: '0x0000000000000000000000000000000000000000'
};
const operator = await operatorFactory.deployOperator(config);
```
### Payment Flow
```mermaid theme={null}
sequenceDiagram
participant Buyer
participant Seller
participant Arbiter
participant Operator
participant Escrow
Note over Buyer,Escrow: Day 0 - Order and Ship
Buyer->>Escrow: authorize(payment)
Note over Escrow: Funds locked for 14 days
Seller->>Seller: Ships product
Note over Buyer,Escrow: Day 10 - Product Arrives
Note over Buyer: Inspects and finds defect
Seller->>Operator: freeze(paymentId)
Note over Operator: Payment frozen for 5 days
Note over Buyer,Escrow: Day 14 - Escrow Ends
Note over Operator: Still frozen
Note over Buyer,Escrow: Day 15 - Dispute
Buyer->>Arbiter: Report issue
Arbiter->>Arbiter: Investigates
Note over Buyer,Escrow: Day 16 - Resolution
Arbiter->>Operator: void(paymentInfo, data)
Operator->>Escrow: void(paymentInfo)
Escrow->>Buyer: Full refund
```
***
## Example 4: Service-Based Payments (Milestone Capture)
**Use Case:** Freelance work with milestone-based releases. Receiver can trigger partial releases.
### Configuration
```typescript theme={null}
// Deploy arbiter condition for disputes
const arbiterCondition = await new StaticAddressCondition(arbiterAddress);
// 3-day escrow per milestone (no freeze)
const escrowPeriod = await escrowPeriodFactory.deploy(
3 * 24 * 60 * 60, // 3 days per milestone
zeroHash // bytes32(0) = operator-only
);
// Receiver can capture after short escrow
const capturePreActionCondition = await new AndCondition([
RECEIVER_CONDITION, // Only receiver
escrowPeriod // Escrow period passed
]);
const config = {
feeReceiver: arbiterAddress, // Arbiter earns fees
feeCalculator: feeCalculatorAddress, // Operator fee calculator
authorizePreActionCondition: PAYER_CONDITION, // Only payer authorizes
authorizePostActionHook: escrowPeriod, // Record auth time
chargePreActionCondition: RECEIVER_CONDITION, // Receiver can charge partials
chargePostActionHook: '0x0000000000000000000000000000000000000000',
capturePreActionCondition: capturePreActionCondition,
capturePostActionHook: '0x0000000000000000000000000000000000000000',
voidPreActionCondition: arbiterCondition.address,
voidPostActionHook: '0x0000000000000000000000000000000000000000',
refundPreActionCondition: arbiterCondition.address,
refundPostActionHook: '0x0000000000000000000000000000000000000000'
};
const operator = await operatorFactory.deployOperator(config);
```
### Payment Flow
```mermaid theme={null}
sequenceDiagram
participant Client
participant Freelancer
participant Operator
participant Escrow
Note over Client,Escrow: Day 0 - Project Start
Client->>Escrow: authorize(1000 USDC)
Note over Escrow: Funds locked
Note over Client,Escrow: Day 5 - Milestone 1
Freelancer->>Operator: charge(300 USDC)
Operator->>Escrow: capture partial
Escrow->>Freelancer: 300 USDC
Note over Escrow: 700 USDC remaining
Note over Client,Escrow: Day 10 - Milestone 2
Freelancer->>Operator: charge(400 USDC)
Operator->>Escrow: capture partial
Escrow->>Freelancer: 400 USDC
Note over Escrow: 300 USDC remaining
Note over Client,Escrow: Day 15 - Final Milestone
Freelancer->>Operator: capture(paymentInfo, amount)
Operator->>Escrow: capture remaining
Escrow->>Freelancer: 300 USDC
Note over Escrow: Payment complete
```
***
## Example 5: Arbiter-Controlled Escrow
**Use Case:** Fully managed escrow service where arbiter controls all actions.
### Configuration
```typescript theme={null}
// Deploy arbiter condition
const arbiterCondition = await new StaticAddressCondition(arbiterAddress);
const config = {
feeReceiver: arbiterAddress, // Arbiter earns all fees
feeCalculator: feeCalculatorAddress, // Operator fee calculator
authorizePreActionCondition: arbiterCondition.address, // Arbiter creates payments
authorizePostActionHook: '0x0000000000000000000000000000000000000000',
chargePreActionCondition: arbiterCondition.address, // Arbiter charges
chargePostActionHook: '0x0000000000000000000000000000000000000000',
capturePreActionCondition: arbiterCondition.address, // Arbiter releases
capturePostActionHook: '0x0000000000000000000000000000000000000000',
voidPreActionCondition: arbiterCondition.address, // Arbiter refunds
voidPostActionHook: '0x0000000000000000000000000000000000000000',
refundPreActionCondition: arbiterCondition.address,
refundPostActionHook: '0x0000000000000000000000000000000000000000'
};
const operator = await operatorFactory.deployOperator(config);
```
Factory-level fees still apply (MAX\_TOTAL\_FEE\_RATE and PROTOCOL\_FEE\_PERCENTAGE). This example assumes protocol takes no cut at factory level.
**Use cases:**
* Professional escrow services
* Legal settlements
* High-value transactions requiring oversight
***
## Example 6: Receiver-Initiated Refunds
**Use Case:** Receiver can offer refunds (for example, a return policy).
### Configuration
```typescript theme={null}
// Deploy arbiter condition
const arbiterCondition = await new StaticAddressCondition(arbiterAddress);
// 7-day escrow (no freeze)
const escrowPeriod = await escrowPeriodFactory.deploy(
7 * 24 * 60 * 60,
zeroHash // bytes32(0) = operator-only
);
// Receiver OR Arbiter can capture
const capturePreActionCondition = await new AndCondition([
await new OrCondition([RECEIVER_CONDITION, arbiterCondition.address]),
escrowPeriod // Escrow period passed
]);
// Receiver OR Arbiter can refund
const refundCondition = await new OrCondition([
RECEIVER_CONDITION,
arbiterCondition.address
]);
const config = {
feeReceiver: arbiterAddress, // Arbiter earns fees
feeCalculator: feeCalculatorAddress, // Operator fee calculator
authorizePreActionCondition: ALWAYS_TRUE_CONDITION,
authorizePostActionHook: escrowPeriod, // Record auth time
chargePreActionCondition: '0x0000000000000000000000000000000000000000',
chargePostActionHook: '0x0000000000000000000000000000000000000000',
capturePreActionCondition: capturePreActionCondition,
capturePostActionHook: '0x0000000000000000000000000000000000000000',
voidPreActionCondition: refundCondition, // Receiver OR Arbiter
voidPostActionHook: '0x0000000000000000000000000000000000000000',
refundPreActionCondition: RECEIVER_CONDITION, // Only receiver after capture
refundPostActionHook: '0x0000000000000000000000000000000000000000'
};
const operator = await operatorFactory.deployOperator(config);
```
### Payment Flow
```mermaid theme={null}
sequenceDiagram
participant Buyer
participant Seller
participant Operator
participant Escrow
Note over Buyer,Escrow: Day 0 - Purchase
Buyer->>Escrow: authorize(payment)
Note over Escrow: Funds locked for 7 days
Seller->>Buyer: Ships product
Note over Buyer,Escrow: Day 3 - Product Received
Note over Buyer: Receives product
Note over Buyer,Escrow: Day 5 - Return Request
Buyer->>Seller: Requests return
Note over Seller: Approves return
Seller->>Operator: void(paymentInfo, data)
Operator->>Escrow: void(paymentInfo)
Escrow->>Buyer: Full refund
Note over Buyer,Escrow: Buyer returns product
```
***
## Example 7: Subscription Payments
**Use Case:** Recurring payments with automatic charge capability.
### Configuration
```typescript theme={null}
// Deploy condition for service provider (no arbiter needed for subscriptions)
const providerCondition = await new StaticAddressCondition(serviceProviderAddress);
// Receiver can charge immediately (no escrow)
const config = {
feeReceiver: serviceProviderAddress, // Service provider earns fees
feeCalculator: feeCalculatorAddress, // Operator fee calculator
authorizePreActionCondition: PAYER_CONDITION, // Payer sets up subscription
authorizePostActionHook: '0x0000000000000000000000000000000000000000',
chargePreActionCondition: providerCondition.address, // Provider charges monthly
chargePostActionHook: '0x0000000000000000000000000000000000000000',
capturePreActionCondition: providerCondition.address,// Provider releases
capturePostActionHook: '0x0000000000000000000000000000000000000000',
voidPreActionCondition: '0x0000000000000000000000000000000000000000', // Open-access: anyone can call void() (`address(0)` = default-allow)
voidPostActionHook: '0x0000000000000000000000000000000000000000',
refundPreActionCondition: '0x0000000000000000000000000000000000000000',
refundPostActionHook: '0x0000000000000000000000000000000000000000'
};
const operator = await operatorFactory.deployOperator(config);
```
For subscriptions with dispute resolution, deploy an arbiter condition and use it for refund conditions. This example shows a simple subscription without arbiter.
**Authorization Expiry for Subscriptions:** When authorizing, set `authorizationExpiry` to limit how long the service provider can charge. For example, a 12-month subscription would set expiry to `block.timestamp + 365 days`. After expiry, the payer can reclaim any unused funds by calling `void()`.
### Payment Flow
```mermaid theme={null}
sequenceDiagram
participant User
participant Provider
participant Operator
participant Escrow
Note over User,Escrow: Day 0 - Subscription Setup
User->>Escrow: authorize(1200 USDC)
Note over Escrow: Funds locked
Note over User,Escrow: Month 1 - First Charge
Provider->>Operator: charge(100 USDC)
Operator->>Escrow: capture partial
Escrow->>Provider: 100 USDC
Note over User,Escrow: Months 2-11 - Continues
Provider->>Operator: charge(100 USDC)
Operator->>Escrow: capture partial
Escrow->>Provider: 100 USDC
Note over User,Escrow: Month 12 - Subscription Ends
Provider->>Operator: capture(paymentInfo, amount)
Operator->>Escrow: capture remaining
Escrow->>Provider: Remaining funds
Note over User,Escrow: Alt: If cancelled early
Note over User: Payer reclaims after authorizationExpiry
User->>Escrow: void(paymentId)
Escrow->>User: Unused funds
```
***
## Example 8: DAO Treasury Controlled
**Use Case:** DAO manages grant releases via multisig governance. No arbiter needed: the DAO is the authority.
### Configuration
```typescript theme={null}
// Deploy condition for DAO multisig
const daoCondition = await new StaticAddressCondition(DAO_MULTISIG_ADDRESS);
const config = {
feeReceiver: DAO_MULTISIG_ADDRESS, // DAO treasury earns fees
feeCalculator: feeCalculatorAddress, // Operator fee calculator
authorizePreActionCondition: daoCondition.address, // DAO authorizes grants
authorizePostActionHook: '0x0000000000000000000000000000000000000000',
chargePreActionCondition: '0x0000000000000000000000000000000000000000',
chargePostActionHook: '0x0000000000000000000000000000000000000000',
capturePreActionCondition: daoCondition.address, // DAO must approve releases
capturePostActionHook: '0x0000000000000000000000000000000000000000',
voidPreActionCondition: daoCondition.address, // DAO can refund if needed
voidPostActionHook: '0x0000000000000000000000000000000000000000',
refundPreActionCondition: '0x0000000000000000000000000000000000000000', // Open-access: anyone can call refund() (`address(0)` = default-allow)
refundPostActionHook: '0x0000000000000000000000000000000000000000'
};
const operator = await operatorFactory.deployOperator(config);
```
### Payment Flow
```mermaid theme={null}
sequenceDiagram
participant Grantee
participant DAO
participant Operator
participant Escrow
Note over Grantee,Escrow: Day 0 - Grant Authorization
DAO->>Escrow: authorize(50000 USDC)
Note over Escrow: Funds locked
Note over Grantee: Begins work
Note over Grantee,Escrow: Months 1-3 - Development
Note over Grantee: Works on milestone
Note over Grantee,Escrow: Month 3 - Completion
Grantee->>DAO: Submits deliverables
Note over DAO: Review and vote
DAO->>DAO: Multisig approval
Note over Grantee,Escrow: Capture
DAO->>Operator: capture(paymentInfo, amount)
Operator->>Escrow: capture funds
Escrow->>Grantee: 50000 USDC
Note over Grantee: Grant complete
```
**Benefits:**
* Governance-controlled releases
* No third-party arbiter needed
* DAO earns fees back to treasury
* On-chain transparent decision making
***
## Example 9: Platform-Controlled Streaming Payments
**Use Case:** Platform manages time-proportional streaming payments. Users can cancel anytime.
### Configuration
```typescript theme={null}
// Deploy condition for platform address
const platformCondition = await new StaticAddressCondition(PLATFORM_ADDRESS);
// Time-proportional charge condition (custom, not provided shipped with the SDK,
// this is a hypothetical custom condition you would implement yourself)
const timeProportionalCondition = await new TimeProportionalCondition();
const config = {
feeReceiver: PLATFORM_ADDRESS, // Platform earns fees
feeCalculator: feeCalculatorAddress, // Operator fee calculator
authorizePreActionCondition: PAYER_CONDITION, // Payer authorizes stream
authorizePostActionHook: '0x0000000000000000000000000000000000000000',
chargePreActionCondition: new AndCondition([
RECEIVER_CONDITION,
timeProportionalCondition // Can only charge proportional to time
]),
chargePostActionHook: '0x0000000000000000000000000000000000000000',
capturePreActionCondition: RECEIVER_CONDITION, // Receiver releases remaining
capturePostActionHook: '0x0000000000000000000000000000000000000000',
voidPreActionCondition: PAYER_CONDITION, // Payer can cancel stream
voidPostActionHook: '0x0000000000000000000000000000000000000000',
refundPreActionCondition: '0x0000000000000000000000000000000000000000', // Open-access: anyone can call refund() (`address(0)` = default-allow)
refundPostActionHook: '0x0000000000000000000000000000000000000000'
};
const operator = await operatorFactory.deployOperator(config);
```
### Payment Flow
```mermaid theme={null}
sequenceDiagram
participant User
participant Platform
participant Operator
participant Escrow
Note over User,Escrow: Day 0 - Stream Authorization
User->>Escrow: authorize(10000 USDC)
Note over Escrow: Funds locked for 100 days
Note over User,Escrow: Day 10 - First Charge
Platform->>Operator: charge(1000 USDC)
Note over Operator: Time check passes
Operator->>Escrow: capture 1000 USDC
Escrow->>Platform: 1000 USDC
Note over Escrow: 9000 USDC remaining
Note over User,Escrow: Day 20 - Second Charge
Platform->>Operator: charge(1000 USDC)
Operator->>Escrow: capture 1000 USDC
Escrow->>Platform: 1000 USDC
Note over Escrow: 8000 USDC remaining
Note over User,Escrow: Day 30 - Cancellation
User->>Operator: void(paymentId)
Operator->>Escrow: void remaining
Escrow->>User: 8000 USDC refund
Note over Escrow: Stream ended
```
**Benefits:**
* Time-based fairness (can't charge ahead of time)
* User can cancel anytime
* No arbiter needed
* Platform controls no disputes
***
## Example 10: Self-Service Invoice Payment (No Arbiter)
**Use Case:** B2B invoice payments with no disputes. Companies trust each other directly.
### Configuration
```typescript theme={null}
// No arbiter - receiver controls capture, platform earns fees
const platformCondition = await new StaticAddressCondition(PLATFORM_ADDRESS);
const config = {
feeReceiver: PLATFORM_ADDRESS, // Platform earns fees
feeCalculator: feeCalculatorAddress, // Operator fee calculator
authorizePreActionCondition: PAYER_CONDITION, // Payer creates invoice payment
authorizePostActionHook: '0x0000000000000000000000000000000000000000',
chargePreActionCondition: RECEIVER_CONDITION, // Receiver charges on delivery
chargePostActionHook: '0x0000000000000000000000000000000000000000',
capturePreActionCondition: RECEIVER_CONDITION, // Receiver releases on payment terms
capturePostActionHook: '0x0000000000000000000000000000000000000000',
voidPreActionCondition: RECEIVER_CONDITION,// Receiver can refund (invoice error)
voidPostActionHook: '0x0000000000000000000000000000000000000000',
refundPreActionCondition: '0x0000000000000000000000000000000000000000', // Open-access: anyone can call refund() (`address(0)` = default-allow)
refundPostActionHook: '0x0000000000000000000000000000000000000000'
};
const operator = await operatorFactory.deployOperator(config);
```
### Payment Flow
```mermaid theme={null}
sequenceDiagram
participant CompanyA as Company A
participant CompanyB as Company B
participant Operator
participant Escrow
participant Platform
Note over CompanyA,Platform: Day 0 - Invoice Creation
CompanyA->>Escrow: authorize(100000 USDC)
Note over Escrow: Funds locked for Net 30
CompanyB->>CompanyB: Ships goods
Note over CompanyA,Platform: Days 1-29 - Delivery
Note over CompanyA: Receives goods
Note over CompanyA,Platform: Day 30 - Settlement
CompanyB->>Operator: capture(paymentInfo, amount)
Operator->>Escrow: capture funds
Escrow->>CompanyB: 99900 USDC
Escrow->>Platform: 100 USDC
Note over CompanyB: Invoice settled
```
**Benefits:**
* Trusted B2B relationship (no arbiter overhead)
* Standard payment terms enforced on-chain
* Receiver can self-correct invoice errors
* Platform monetizes via fees only
***
## Fee Configuration Comparison
| Use Case | Max Fee (bps) | Protocol % | Operator % | Fee Recipient | Total on 1000 USDC |
| --------------------------- | ------------- | ---------- | ---------- | ---------------- | ------------------ |
| E-Commerce (Arbiter) | 5 (0.05%) | 25% | 75% | Arbiter | 0.50 USDC |
| Instant Payment (Arbiter) | 10 (0.1%) | 50% | 50% | Arbiter | 1.00 USDC |
| Physical Goods (Arbiter) | 3 (0.03%) | 20% | 80% | Arbiter | 0.30 USDC |
| Service/Milestone (Arbiter) | 8 (0.08%) | 30% | 70% | Arbiter | 0.80 USDC |
| Managed Escrow (Arbiter) | 20 (0.2%) | 0% | 100% | Arbiter | 2.00 USDC |
| Receiver Refunds (Arbiter) | 5 (0.05%) | 25% | 75% | Arbiter | 0.50 USDC |
| Subscription (Provider) | 15 (0.15%) | 40% | 60% | Service Provider | 1.50 USDC |
| DAO Grants (DAO) | 5 (0.05%) | 20% | 80% | DAO Treasury | 0.50 USDC |
| Streaming (Platform) | 10 (0.1%) | 30% | 70% | Platform | 1.00 USDC |
| B2B Invoice (Platform) | 10 (0.1%) | 50% | 50% | Platform | 1.00 USDC |
***
## Configuration Checklist
Before deploying, verify:
* [ ] Freeze policy suits your use case
* [ ] Escrow period is appropriate for delivery time
* [ ] Capture condition prevents premature captures
* [ ] Refund conditions allow arbiter intervention
* [ ] Fee rates are competitive and sustainable
* [ ] Protocol fee percentage is reasonable
* [ ] Tested on testnet with same configuration
* [ ] A trusted party controls the arbiter address (preferably multisig)
* [ ] Verify condition contracts on the block explorer
***
## Testing Your Configuration
```typescript theme={null}
import { createTestClient, http, parseUnits, keccak256, toHex } from 'viem';
import { baseSepolia } from 'viem/chains';
import { paymentOperatorAbi } from '@x402r/core';
// Deploy on Base Sepolia first
const operatorAddress = await factory.write.deployOperator([config]);
const testClient = createTestClient({
chain: baseSepolia,
transport: http(),
mode: 'anvil',
});
// 1. Authorize
await walletClient.writeContract({
address: operatorAddress,
abi: paymentOperatorAbi,
functionName: 'authorize',
args: [paymentInfo, parseUnits('100', 6), tokenCollectorAddress, collectorData],
});
// 2. Try to capture immediately (should fail if escrow configured)
// Expect revert with ConditionNotMet
try {
await walletClient.writeContract({
address: operatorAddress,
abi: paymentOperatorAbi,
functionName: 'capture',
args: [paymentInfo, parseUnits('100', 6)],
});
} catch (e) {
console.log('Expected revert: escrow period not passed');
}
// 3. Fast forward time (requires Anvil/Hardhat test node)
await testClient.increaseTime({ seconds: 7 * 24 * 60 * 60 });
await testClient.mine({ blocks: 1 });
// 4. Capture after escrow
await walletClient.writeContract({
address: operatorAddress,
abi: paymentOperatorAbi,
functionName: 'capture',
args: [paymentInfo, parseUnits('100', 6)],
});
// Verify funds transferred correctly
```
***
## Best Practices
Use well-tested configurations for your first deployments:
* Standard 7-day escrow
* 3-day payer freeze
* Arbiter-only refunds
* Low fee rates (3-5 bps)
Research competitors' escrow periods and fees:
* E-commerce: 3-7 days typical
* Freelance: 3-14 days per milestone
* High-value: 14-30 days common
Test your configuration handles:
* Immediate capture attempts
* Freeze during escrow
* Freeze expiry
* Refunds in both states
* Fee distribution
* Partial charges followed by capture
Keep records of deployed configurations:
```json theme={null}
{
"operator": "0x...",
"arbiter": "0x...",
"escrowPeriod": "7 days",
"freezeDuration": "3 days",
"freezeBy": "payer",
"releaseBy": "receiver OR arbiter",
"maxFeeBps": 5,
"protocolFeePct": 25,
"network": "base-mainnet",
"deployedAt": "2025-01-25"
}
```
## Next Steps
Review contract methods and security features.
Learn more about custom conditions.
Run working TypeScript examples for each role.
Deploy these configurations using the SDK.
# Factories
Source: https://docs.x402r.org/contracts/factories
Factory patterns, CREATE2 deployments, and deterministic addresses
## Overview
x402r uses the factory pattern with CREATE2 for gas-efficient, deterministic contract deployments. Factories enable on-demand instance creation with predictable addresses.
## Why factories
Addresses are predictable before deployment, enabling:
* Off-chain address generation
* Cross-chain address consistency
* Contract-to-contract communication without registries
Many instances can share immutable configuration:
* Lower deployment costs
* Consistent behavior across instances
* Centralized ownership control
Calling a factory with the same parameters returns the existing contract:
* Safe to call again
* No duplicate deployments
* Built-in deduplication
Singleton conditions deployed once, reused everywhere:
* PayerCondition, ReceiverCondition deployed once
* All operators share the same condition instances
* Minimal storage overhead
## Payment Operator Factory
Deploys PaymentOperator instances with deterministic addresses.
### Contract Address
All factories use universal CREATE2 addresses (same on every chain).
**PaymentOperatorFactory:** `0xa0d4734842df1690a5B33Cb21828c946e39D55a2`
### Configuration Structure
```solidity theme={null}
struct OperatorConfig {
address feeReceiver; // Who receives operator fees
address feeCalculator; // Operator fee calculator (IFeeCalculator)
address authorizePreActionCondition;
address authorizePostActionHook;
address chargePreActionCondition;
address chargePostActionHook;
address capturePreActionCondition;
address capturePostActionHook;
address voidPreActionCondition;
address voidPostActionHook;
address refundPreActionCondition;
address refundPostActionHook;
}
```
### Deployment Method
```solidity theme={null}
function deployOperator(
OperatorConfig calldata config
) external returns (address operator)
```
**Parameters (in config):**
* `feeReceiver` - Who receives operator fees (arbiter, service provider, or treasury)
* `authorizePreActionCondition` through `refundPostActionHook` - 10-slot configuration
**Note:** the factory sets `maxFeeBps` and `protocolFeePct` (shared across all operators)
**Returns:** Address of deployed operator (or existing if already deployed)
### Address Prediction
Predict the operator address before deployment:
```solidity theme={null}
function computeAddress(
OperatorConfig calldata config
) external view returns (address)
```
**Usage:**
```typescript theme={null}
const config = {
feeReceiver: arbiterAddress,
authorizePreActionCondition: ALWAYS_TRUE_CONDITION,
// ... rest of config
};
const predictedAddress = await factory.computeAddress(config);
console.log("Operator will be deployed at:", predictedAddress);
// Deploy - will use same address
const deployedAddress = await factory.deployOperator(config);
assert(deployedAddress === predictedAddress);
```
### Example Deployment
#### Marketplace Operator
```typescript theme={null}
import { createWalletClient, http, getContract, zeroAddress } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import { paymentOperatorAbi } from '@x402r/core';
const FACTORY_ADDRESS = '0xa0d4734842df1690a5B33Cb21828c946e39D55a2';
const account = privateKeyToAccount('0x...');
const walletClient = createWalletClient({
account,
chain: base,
transport: http()
});
const factory = getContract({
address: FACTORY_ADDRESS,
abi: paymentOperatorAbi,
client: walletClient
});
// Deploy condition for arbiter via factory
const arbiterConditionHash = await staticAddressConditionFactory.write.deploy([arbiterAddress]);
const arbiterConditionAddress = /* get from receipt */;
// Deploy capture condition: arbiter AND escrow period passed
const capturePreActionConditionHash = await andConditionFactory.write.deploy([
[arbiterConditionAddress, escrowPeriodAddress]
]);
const captureConditionAddress = /* get from receipt */;
// Define configuration
const config = {
feeReceiver: arbiterAddress, // Arbiter earns fees
feeCalculator: feeCalculatorAddress,
authorizePreActionCondition: ALWAYS_TRUE_CONDITION,
authorizePostActionHook: escrowPeriodAddress,
chargePreActionCondition: zeroAddress, // Default allow
chargePostActionHook: zeroAddress, // No recording
capturePreActionCondition: captureConditionAddress,
capturePostActionHook: zeroAddress,
voidPreActionCondition: arbiterConditionAddress,
voidPostActionHook: zeroAddress,
refundPreActionCondition: arbiterConditionAddress,
refundPostActionHook: zeroAddress
};
// Deploy operator
const hash = await factory.write.deployOperator([config]);
const receipt = await walletClient.waitForTransactionReceipt({ hash });
const operatorAddress = receipt.logs[0].address;
console.log("Deployed marketplace operator at:", operatorAddress);
```
#### Subscription Operator
```typescript theme={null}
// Deploy condition for service provider
const providerCondition = await new StaticAddressCondition(serviceProviderAddress);
const config = {
feeReceiver: serviceProviderAddress, // Provider earns fees
authorizePreActionCondition: PAYER_CONDITION,
authorizePostActionHook: zeroAddress,
chargePreActionCondition: providerCondition.address,
chargePostActionHook: zeroAddress,
capturePreActionCondition: providerCondition.address,
capturePostActionHook: zeroAddress,
voidPreActionCondition: zeroAddress, // No refunds
voidPostActionHook: zeroAddress,
refundPreActionCondition: zeroAddress,
refundPostActionHook: zeroAddress
};
const hash = await factory.write.deployOperator([config]);
console.log("Deployed subscription operator, tx:", hash);
```
If you call `deployOperator()` with the same configuration twice, the factory returns the existing operator address without deploying a new contract.
***
## Escrow Period Factory
Deploys `EscrowPeriod` contracts - combined hook and condition for time-based capture logic.
### Contract Address
**EscrowPeriodFactory:** `0xe72D2014ebC48F1d92521e8629574918E8030548`
### Deployment Method
```solidity theme={null}
function deploy(
uint256 escrowPeriod,
bytes32 authorizedCodehash
) external returns (address escrowPeriodAddr)
```
**Parameters:**
* `escrowPeriod` - Duration in seconds (for example, `7 * 24 * 60 * 60` for 7 days)
* `authorizedCodehash` - Runtime codehash of authorized caller (`bytes32(0)` = operator-only)
**Returns:** Address of deployed EscrowPeriod contract
### How It Works
The factory deploys a single **EscrowPeriod** contract that:
* Extends `AuthorizationTimeRecorderHook` (implements `IHook`)
* Implements `ICondition`
* Records authorization timestamp when used as hook
* Checks if escrow period has passed when used as condition
**Architecture:**
```mermaid theme={null}
flowchart LR
EP[EscrowPeriod] -->|extends| ATR[AuthorizationTimeRecorderHook]
EP -->|implements| IC[ICondition]
ATR -->|implements| IR[IHook]
```
Use the SAME `EscrowPeriod` address for both `AUTHORIZE_POST_ACTION_HOOK` and `CAPTURE_PRE_ACTION_CONDITION` slots on the operator. For freeze functionality, deploy a separate `Freeze` condition and compose via `AndCondition([escrowPeriod, freeze])`.
### Example Deployment
```typescript theme={null}
import { getContract, zeroHash } from 'viem';
const factory = getContract({
address: ESCROW_PERIOD_FACTORY_ADDRESS,
abi: EscrowPeriodFactory.abi,
client: walletClient
});
// Deploy 7-day escrow (operator-only access)
const hash = await factory.write.deploy([
7 * 24 * 60 * 60, // 7 days
zeroHash // bytes32(0) = operator-only
]);
const receipt = await walletClient.waitForTransactionReceipt({ hash });
const escrowPeriodAddress = receipt.logs[0].address;
console.log("EscrowPeriod:", escrowPeriodAddress);
// Use SAME address for both hook and condition
const config = {
authorizePreActionCondition: ALWAYS_TRUE_CONDITION,
authorizePostActionHook: escrowPeriodAddress, // Record auth time
// ...
capturePreActionCondition: escrowPeriodAddress, // Check escrow passed
capturePostActionHook: zeroAddress, // No additional recording needed
// ...
};
```
### Common Escrow Periods
| Use Case | Recommended Period |
| ------------------------------ | --------------------------- |
| Digital goods / services | 1-3 days |
| Physical goods (domestic) | 7-14 days |
| Physical goods (international) | 14-30 days |
| Large purchases / services | 30-60 days |
| No escrow (instant release) | 0 (use different condition) |
***
## Freeze Factory
Deploys `Freeze` condition contracts that block capture when the payer freezes a payment.
### Contract Address
**FreezeFactory:** `0xeC092cf1215DB44af0Abe87c1157E304FEa5d0Eb`
### Deployment Method
```solidity theme={null}
function deploy(
address freezeCondition,
address unfreezeCondition,
uint256 freezeDuration,
address escrowPeriodContract
) external returns (address freezeAddr)
```
**Parameters:**
* `freezeCondition` - ICondition that gates freeze calls (for example, PayerCondition)
* `unfreezeCondition` - ICondition that gates unfreeze calls (for example, PayerCondition or ArbiterCondition)
* `freezeDuration` - How long freeze lasts in seconds (`0` = permanent until unfrozen)
* `escrowPeriodContract` - Address of EscrowPeriod contract (`address(0)` = freeze unconstrained by time)
**Returns:** Address of deployed Freeze condition
### Full Freeze Deployment Example
```typescript theme={null}
// Step 1: Deploy EscrowPeriod (7 days, operator-only recording)
const escrowPeriod = await escrowPeriodFactory.write.deploy([
7 * 24 * 60 * 60, // 7 days
zeroHash // bytes32(0) = operator-only
]);
// Step 2: Deploy Freeze condition (payer freeze/unfreeze, 3-day duration, linked to EscrowPeriod)
const freeze = await freezeFactory.write.deploy([
PAYER_CONDITION, // Only payer can freeze
PAYER_CONDITION, // Only payer can unfreeze (or use ARBITER_CONDITION)
3 * 24 * 60 * 60, // 3 days (auto-expires)
escrowPeriod // Link to EscrowPeriod (or zeroAddress for unconstrained)
]);
// Step 3: Compose with EscrowPeriod for capture condition
const capturePreActionCondition = await andConditionFactory.write.deploy([
[escrowPeriod, freeze]
]);
// Use in operator config
const config = {
// ...
capturePreActionCondition: capturePreActionCondition,
// ...
};
```
### Condition Singletons
Reference the pre-deployed condition singletons (PayerCondition, ReceiverCondition, AlwaysTrueCondition) by their canonical addresses. The full address registry lives on [Periphery Overview: Condition Singletons](/contracts/periphery/overview#condition-singletons), identical across every supported chain.
### Example Deployments
Payer can freeze, arbiter can unfreeze (or it expires after 3 days):
```typescript theme={null}
const freeze = await freezeFactory.deploy(
PAYER_CONDITION, // Only payer can freeze
ARBITER_CONDITION, // Only arbiter can unfreeze
3 * 24 * 60 * 60, // 3 days (auto-expires)
escrowPeriodAddress // Link to EscrowPeriod
);
```
Receiver can freeze, arbiter can unfreeze (or it expires after 5 days):
```typescript theme={null}
const freeze = await freezeFactory.deploy(
RECEIVER_CONDITION, // Only receiver can freeze
ARBITER_CONDITION, // Only arbiter can unfreeze
5 * 24 * 60 * 60, // 5 days (auto-expires)
escrowPeriodAddress // Link to EscrowPeriod
);
```
Either payer or receiver can freeze, both can unfreeze:
```typescript theme={null}
// First deploy OrCondition
const orCondition = await new OrCondition([
PAYER_CONDITION,
RECEIVER_CONDITION
]);
const freeze = await freezeFactory.deploy(
orCondition.address, // Payer OR Receiver can freeze
orCondition.address, // Payer OR Receiver can unfreeze
3 * 24 * 60 * 60, // 3 days
escrowPeriodAddress // Link to EscrowPeriod
);
```
Only arbiter can freeze/unfreeze:
```typescript theme={null}
const freeze = await freezeFactory.deploy(
ARBITER_CONDITION, // Only arbiter can freeze
ARBITER_CONDITION, // Only arbiter can unfreeze
7 * 24 * 60 * 60, // 7 days
escrowPeriodAddress // Link to EscrowPeriod
);
```
### Freeze Duration Guidelines
| Duration | Use Case |
| -------- | --------------------------- |
| 1 day | Quick investigation period |
| 3 days | Standard fraud check window |
| 5-7 days | Extended investigation |
| 14+ days | Complex dispute resolution |
Freeze duration should balance payer protection with receiver UX. Too long and receivers may avoid the platform. Too short and payers can't adequately investigate.
***
## Factory Ownership
A multisig wallet owns all factories for security.
### Owner Capabilities
Factory owners can:
* Update factory configuration (if mutable fields exist)
* Rescue stuck ETH (via `rescueETH()`)
* Transfer ownership (2-step process)
Factory owners **cannot:**
* Change deployed instances
* Pause or stop operations
* Access funds in deployed operators
### Ownership Transfer
```solidity theme={null}
// Current owner initiates
factory.requestOwnershipHandover(newOwner);
// New owner completes (within 48 hours)
factory.completeOwnershipHandover();
```
***
## Gas Costs
Approximate gas costs for factory deployments (Base Sepolia):
| Operation | Gas Cost | USD (at 0.1 gwei, \$3000 ETH) |
| -------------------------------------- | ---------- | ----------------------------- |
| Deploy PaymentOperator | \~2.5M gas | \~\$0.75 |
| Deploy EscrowPeriod (condition + hook) | \~1.8M gas | \~\$0.54 |
| Deploy Freeze | \~1.0M gas | \~\$0.30 |
| Predict address (view call) | 0 gas | \$0.00 |
Use `predict*Address()` functions before deploying to verify addresses off-chain and avoid unnecessary deployments.
***
## CREATE2 Details
### Salt Generation
Each factory uses different salt strategies:
**PaymentOperatorFactory:**
```solidity theme={null}
bytes32 key = keccak256(abi.encode(
config.feeReceiver,
config.feeCalculator,
config.authorizePreActionCondition,
config.authorizePostActionHook,
config.chargePreActionCondition,
config.chargePostActionHook,
config.capturePreActionCondition,
config.capturePostActionHook,
config.voidPreActionCondition,
config.voidPostActionHook,
config.refundPreActionCondition,
config.refundPostActionHook
));
```
**EscrowPeriodFactory:**
```solidity theme={null}
bytes32 key = keccak256(abi.encodePacked(escrowPeriod, authorizedCodehash));
bytes32 salt = keccak256(abi.encodePacked("escrowPeriod", key));
```
**FreezeFactory:**
```solidity theme={null}
bytes32 key = keccak256(abi.encodePacked(freezeCondition, unfreezeCondition, freezeDuration, escrowPeriodContract));
bytes32 salt = keccak256(abi.encodePacked("freeze", key));
```
### Cross-Chain Addresses
Because the factory uses CREATE2, the same configuration produces the same operator address on any chain where the factory itself lives at the canonical address. As supported chains expand beyond Base, an operator deployed with identical config will land at the same address on each new chain without the integrator needing per-chain bookkeeping.
This enables:
* Consistent addressing across chains
* Simplified multi-chain integrations
* Predictable contract locations
***
## Best Practices
### 1. Predict Before Deploy
Always verify predicted address before deployment:
```typescript theme={null}
const predicted = await factory.read.computeAddress([config]);
const hash = await factory.write.deployOperator([config]);
const receipt = await walletClient.waitForTransactionReceipt({ hash });
// deployed address matches predicted
```
### 2. Reuse Condition Singletons
Don't deploy new PayerCondition/ReceiverCondition - use existing singletons:
```typescript theme={null}
// ✅ Good: Reuse singleton
const config = {
authorizePreActionCondition: PAYER_CONDITION, // Pre-deployed singleton
// ...
};
// ❌ Bad: Deploy new instance
const payerCondition = await new PayerCondition();
const config = {
authorizePreActionCondition: payerCondition.address, // Wastes gas
// ...
};
```
### 3. Test Configuration First
Deploy on testnet with same configuration before mainnet:
```typescript theme={null}
// Test on Base Sepolia first
const testHash = await testnetFactory.write.deployOperator([config]);
// ... test thoroughly ...
// Deploy on mainnet with identical config (same address)
const mainnetHash = await mainnetFactory.write.deployOperator([config]);
```
### 4. Document your config
Keep a record of your deployed configurations:
```typescript theme={null}
const deployments = {
"marketplace-arbiter": {
arbiter: "0x...",
operator: "0x...",
escrowPeriod: 7 * 24 * 60 * 60,
freezeDuration: 3 * 24 * 60 * 60,
maxFeeBps: 5,
protocolFeePct: 25,
network: "base-sepolia"
}
};
```
## Next Steps
Learn about the pluggable condition system.
See real-world configuration examples.
Use the SDK's `deployMarketplaceOperator()` for simplified deployment.
Install the SDK packages.
# Fee System
Source: https://docs.x402r.org/contracts/fees
Additive modular fee architecture with protocol and operator fee layers
## Overview
x402r uses an **additive modular** fee system: `totalFee = protocolFee + operatorFee`. Each layer is independently configurable, and the operator splits fees between a shared protocol recipient and a per-operator fee recipient.
## Fee Architecture
```mermaid theme={null}
flowchart TD
P["Payment: 1000 USDC"] --> CALC["_calculateFees()"]
CALC --> PF["Protocol Fee: 5.00 USDC (50 bps)
via ProtocolFeeConfig"]
CALC --> OF["Operator Fee: 25.00 USDC (250 bps)
via FEE_CALCULATOR"]
PF --> TF["Total Fee: 30.00 USDC (300 bps / 3%)"]
OF --> TF
TF --> REC["Receiver Gets
970.00 USDC"]
TF --> ACC["Fees Accumulate
in Operator Contract"]
ACC --> DIST["distributeFees()"]
DIST --> PR["protocolFeeRecipient
(on ProtocolFeeConfig)"]
DIST --> FR["FEE_RECEIVER
(on Operator)"]
```
### Two Fee Layers
| Layer | Configured By | Mutability | Recipient |
| ---------------- | ------------------------------- | ---------------------------------------- | ------------------------------------------- |
| **Protocol Fee** | `ProtocolFeeConfig` (shared) | Swappable calculator with 7-day timelock | `protocolFeeRecipient` on ProtocolFeeConfig |
| **Operator Fee** | `FEE_CALCULATOR` (per-operator) | Immutable: set at deploy time | `FEE_RECEIVER` on operator |
### Example Calculation
For a 1000 USDC payment with 50 bps protocol fee + 250 bps operator fee:
| Component | Rate | Amount | Goes To |
| ----------------- | ---------------- | --------------- | ---------------------- |
| Protocol Fee | 50 bps (0.5%) | 5.00 USDC | `protocolFeeRecipient` |
| Operator Fee | 250 bps (2.5%) | 25.00 USDC | `FEE_RECEIVER` |
| **Total Fee** | **300 bps (3%)** | **30.00 USDC** | |
| **Receiver Gets** | | **970.00 USDC** | Payment receiver |
## IFeeCalculator Interface
Both protocol and operator fees use the same interface:
```solidity theme={null}
interface IFeeCalculator {
/// @notice Calculate fee in basis points for a payment action
/// @param paymentInfo The payment info struct
/// @param amount The payment amount
/// @param caller The address initiating the action
/// @return feeBps The fee in basis points (e.g., 50 = 0.5%)
function calculateFee(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 amount,
address caller
) external view returns (uint256 feeBps);
}
```
This enables flexible fee models, static rates, volume-based tiers, per-token pricing, or any custom logic.
## StaticFeeCalculator
The simplest implementation, returns a fixed basis points value for every payment:
```solidity theme={null}
contract StaticFeeCalculator is IFeeCalculator {
uint256 public immutable FEE_BPS;
constructor(uint256 _feeBps) {
if (_feeBps > 10000) revert FeeTooHigh();
FEE_BPS = _feeBps;
}
function calculateFee(
AuthCaptureEscrow.PaymentInfo calldata,
uint256,
address
) external view override returns (uint256 feeBps) {
return FEE_BPS;
}
}
```
Deploy via `StaticFeeCalculatorFactory` for deterministic CREATE2 addresses:
```typescript theme={null}
// Deploy a 250 bps (2.5%) fee calculator
const calculatorAddress = await staticFeeCalculatorFactory.write.deploy([250]);
// Same fee rate = same address (idempotent)
const sameAddress = await staticFeeCalculatorFactory.write.deploy([250]);
```
## Fee Locking
The operator **locks fees at authorization time** so later protocol fee changes don't break already-authorized payments.
```solidity theme={null}
struct AuthorizedFees {
uint16 totalFeeBps; // Combined protocol + operator
uint16 protocolFeeBps; // Stored separately for accurate distribution
}
mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees;
```
**Flow:**
1. `authorize()` calculates fees and stores them in `authorizedFees[hash]`
2. `capture()` uses the stored fees, not the current calculator rates
3. Protocol fee timelocks can't break already-authorized payments
`charge()` calculates fees inline since it authorizes and captures atomically, there's no gap where fees could change.
## Fee Bounds Validation
Payers commit to an acceptable fee range via `minFeeBps` and `maxFeeBps` in `PaymentInfo`. The operator validates at `authorize()` and `charge()` time:
```solidity theme={null}
(uint16 totalFeeBps, uint16 protocolFeeBps) = _calculateFees(paymentInfo, amount);
if (totalFeeBps < paymentInfo.minFeeBps || totalFeeBps > paymentInfo.maxFeeBps) {
revert FeeBoundsIncompatible(totalFeeBps, paymentInfo.minFeeBps, paymentInfo.maxFeeBps);
}
```
This ensures payers always know the fee range they're agreeing to.
## Fee Distribution
Fees accumulate in the operator contract. Call `distributeFees()` to disburse them:
```solidity theme={null}
// Anyone can call to distribute fees for a token
operator.distributeFees(usdcAddress);
// Protocol share → protocolFeeRecipient
// Operator share → FEE_RECEIVER
```
**How it works:**
1. Check operator's token balance
2. Protocol share = `accumulatedProtocolFees[token]` (tracked per-token)
3. Operator share = remaining balance
4. Transfer protocol share to `protocolFeeRecipient`
5. Transfer operator share to `FEE_RECEIVER`
6. Reset accumulated tracking to 0
`distributeFees()` is permissionless, anyone can trigger distribution. This stops fees from accumulating indefinitely in the operator.
## ProtocolFeeConfig
Shared protocol-level fee governance with built-in safety:
### Constants
| Parameter | Value |
| ---------------------- | -------- |
| `MAX_PROTOCOL_FEE_BPS` | 500 (5%) |
| `TIMELOCK_DELAY` | 7 days |
### Calculator Changes (7-Day Timelock)
```typescript theme={null}
// Step 1: Queue new calculator
await protocolFeeConfig.queueCalculator(newCalculatorAddress);
// Emits CalculatorChangeQueued(newCalculator, executeAfter)
// Step 2: Wait 7 days
// Step 3: Execute
await protocolFeeConfig.executeCalculator();
// Emits CalculatorChangeExecuted(newCalculator)
// Or cancel:
await protocolFeeConfig.cancelCalculator();
```
### Recipient Changes (7-Day Timelock)
```typescript theme={null}
// Step 1: Queue new recipient
await protocolFeeConfig.queueRecipient(newRecipientAddress);
// Step 2: Wait 7 days
// Step 3: Execute
await protocolFeeConfig.executeRecipient();
```
Operator fees are **immutable**: set at deploy time via `IFeeCalculator` and `FEE_RECEIVER`. Only protocol fees support updates (with 7-day timelock). Already-authorized payments use locked fee rates regardless.
### Disabling Protocol Fees
Set the protocol fee calculator to `address(0)` to disable protocol fees entirely. The operator will calculate 0 bps for the protocol layer.
## FEE\_RECEIVER Roles
The operator's `FEE_RECEIVER` varies by use case:
| Use Case | FEE\_RECEIVER | Description |
| ------------ | ----------------- | ----------------------------------------- |
| Marketplace | Arbiter address | Arbiter earns fees for dispute resolution |
| Subscription | Service provider | Provider earns fees for service delivery |
| DAO Grants | DAO multisig | Fees return to treasury |
| Platform | Platform treasury | Platform monetizes via fees |
| B2B Invoice | Platform address | Platform earns for facilitation |
## Fee Configuration Comparison
| Use Case | Total Fee | Protocol | Operator | Receiver on 1000 USDC |
| ----------------------- | -------------- | -------- | -------- | --------------------- |
| E-Commerce Marketplace | 300 bps (3%) | 50 bps | 250 bps | 970.00 USDC |
| Task Execution Platform | 1300 bps (13%) | 100 bps | 1200 bps | 870.00 USDC |
| Subscription / SaaS | 500 bps (5%) | 50 bps | 450 bps | 950.00 USDC |
| Managed Escrow | 800 bps (8%) | 0 bps | 800 bps | 920.00 USDC |
| B2B Invoice | 100 bps (1%) | 25 bps | 75 bps | 990.00 USDC |
| Freelance / Gig | 1000 bps (10%) | 100 bps | 900 bps | 900.00 USDC |
## Next Steps
See how fees integrate with PaymentOperator.
Deploy operators with fee configuration.
Complete fee configurations for common use cases.
Understand the full payment flow.
# Gas Costs
Source: https://docs.x402r.org/contracts/gas-costs
Foundry-measured gas costs for every on-chain x402r operation, with per-plugin overhead breakdown
## Overview
x402r adds escrow, refund windows, and dispute resolution on top of the [Commerce Payments Protocol](https://github.com/base/commerce-payments). Below you'll find the **measured gas cost** of every on-chain operation so you can weigh the overhead.
All numbers come from Foundry simulations (`forge test --gas-report`) with optimizer enabled (200 runs, via IR), pinned to [x402r-contracts @ `bb188db`](https://github.com/BackTrackCo/x402r-contracts/commit/bb188dbc0251f9a3af7da57906d5c59e2b2a14d0) (snapshot: 2026-05-20). The benchmark lives at [`test/gas/GasBenchmark.t.sol`](https://github.com/BackTrackCo/x402r-contracts/blob/bb188db/test/gas/GasBenchmark.t.sol). Numbers are per-transaction and warm where the test measures warm (so they reflect typical second-and-beyond payments on the same operator); cold-vs-warm splits are reported alongside the warm number where relevant.
The buyer never pays gas. They only sign an off-chain ERC-3009 or Permit2 authorization. The facilitator, merchant, or another party submits every on-chain transaction.
## What you'll pay on Base
| Role | Operations | Gas | Cost on Base |
| -------------------- | ------------------- | ------- | ------------- |
| **Facilitator** | `authorize()` | 182,440 | \< \$0.005 |
| **Merchant** | `capture()` | 150,049 | \< \$0.005 |
| **Happy path total** | authorize + capture | 332,489 | **\< \$0.01** |
Disputes are rare and add \< \$0.005 with off-chain resolution, see [Dispute Path](#dispute-path) below.
## Happy Path
The happy path has **2 on-chain transactions**: `authorize` (at checkout) and `capture` (after the escrow period expires). With operator fees enabled, the `distributeFees()` call adds a third settle-time write to claim accumulated protocol fees, batched across many payments.
| Operation | Gas | vs transfer | Who Calls | When |
| ------------------ | ------- | ----------- | ----------- | ------------------------------------- |
| `authorize()` | 182,440 | 17.8x | Facilitator | At checkout (HTTP 402 settlement) |
| `capture()` | 150,049 | 14.6x | Anyone | After escrow period expires |
| `distributeFees()` | 57,007 | 5.6x | Owner | Periodically, batched across payments |
The **vs transfer** column shows multiples of a cold ERC-20 `transfer()` (10,263 gas), the absolute floor for moving tokens on-chain.
In production, the merchant typically calls `capture()`, but the function has no caller restriction beyond the configured capture condition (EscrowPeriod + Freeze). After the escrow period passes and the payment isn't frozen, anyone can trigger it.
An escrow authorization is inherently more work than a raw ERC-20 transfer: it validates payment info, checks fee bounds, locks fees, transfers tokens into escrow, and records state. The per-plugin section below shows exactly where the gas goes.
**Facilitators: set a gas limit.** The facilitator pays gas for `authorize()`, but the operator chooses which conditions and hooks to run. Each plugin slot adds cost, and custom plugins can run arbitrary computation. Simulate the transaction with `eth_estimateGas` before submitting and reject operators whose `authorize()` exceeds a reasonable threshold (for example, 300,000 gas). The full x402r configuration uses around 182,000 gas; anything well above that warrants investigation.
## Per-Plugin Gas Costs
The PaymentOperator runs with pluggable conditions (checked before an action) and hooks (called after). You choose which plugins to use. Here's the marginal cost of each, measured by diffing adjacent configurations through the `PaymentOperator` entry point.
### authorize()
| Configuration | Gas | Marginal Cost | Plugin |
| --------------------------- | ------- | ------------- | ------------------------------------------------------------------------------------------------------------------ |
| PaymentOperator, no plugins | 119,018 | : | `bareOperator.authorize()`: operator dispatch, plugin slot checks, escrow `authorize()` call |
| + Fee calculation | 146,738 | **+27,720** | `StaticFeeCalculator`: calculates protocol + operator fees, validates bounds, locks fees in `authorizedFees[hash]` |
| + EscrowPeriod hook | 182,440 | **+35,702** | `EscrowPeriod.run()`: stores `authorizationTime[hash] = block.timestamp` (cold SSTORE to cross-contract slot) |
The EscrowPeriod hook is the single most expensive plugin on `authorize` because it writes to a new storage slot in the EscrowPeriod contract.
### capture()
| Configuration | Gas | Marginal Cost | Plugin |
| -------------------------------- | ------- | ------------- | ------------------------------------------------------------------------------------------------------------------------- |
| PaymentOperator, no plugins | 78,074 | : | `bareOperator.capture()`: operator dispatch + escrow `capture()` call |
| + Fee retrieval and distribution | 117,033 | **+38,959** | Reads locked fees from `authorizedFees[hash]`, calculates protocol share, accumulates in `accumulatedProtocolFees[token]` |
| + ReceiverCondition | 121,529 | **+4,496** | Pure calldata comparison: `caller == paymentInfo.receiver`, no storage reads |
| + EscrowPeriod condition | 126,888 | **+5,359** | Cross-contract SLOAD: reads `authorizationTime[hash]`, compares against `block.timestamp` |
| + Freeze + AndCondition | 147,298 | **+20,410** | AndCondition combinator loop + `Freeze.check()` reads `frozenUntil[hash]` + internal `isDuringEscrowPeriod()` |
**Simple conditions are close to free.** `ReceiverCondition` and `PayerCondition` cost around 4,500 gas; they only compare calldata fields. Cross-contract conditions like `EscrowPeriod` cost around 5,400 because of a cold SLOAD. The `Freeze` condition is the most expensive single condition (+20,410) because of the AndCondition combinator overhead, its own `frozenUntil` storage read, and an internal escrow period check.
## Dispute Path
These operations only happen when a buyer disputes a payment. Most payments never touch this path.
### Off-chain resolution
The refund request, evidence submission, and arbiter approval can all happen off-chain. The only on-chain steps are `freeze()` (to lock the payment during the escrow window) and `void()` (to return funds). The arbiter never submits a transaction; their approval is an EIP-712 signature that anyone can relay.
| On-chain step | Gas | vs transfer | Who Calls |
| ------------- | ----------- | ----------- | --------- |
| `freeze()` | 45,831 | 4.5x | Buyer |
| `void()` | 68,947 | 6.7x | Anyone |
| **Total** | **114,778** | **11.2x** | |
Total dispute cost on Base with off-chain resolution: **\< \$0.005**.
### Fully on-chain fallback
If the parties choose to handle the dispute fully on-chain instead:
| Operation | Gas | vs transfer | Who Calls | Notes |
| ------------------ | ----------- | ----------- | ----------- | ------------------------------------------------------------ |
| `authorize()` | 182,445 | 17.8x | Facilitator | Already paid during happy path |
| `freeze()` | 45,818 | 4.5x | Buyer | Locks payment during escrow window |
| `capture()` | 145,549 | 14.2x | Anyone | Already paid during happy path |
| `requestRefund()` | 418,174 | 40.7x | Buyer | Creates refund request with multi-index storage |
| `submitEvidence()` | 132,431 | 12.9x | Any party | Stores IPFS CID on-chain |
| `deny()` | 11,096 | 1.1x | Arbiter | Terminal status update on the request |
| `refund()` | 57,482 | 5.6x | Anyone | Pulls funds from merchant wallet via ReceiverRefundCollector |
| **Total** | **992,995** | **96.7x** | | |
This total includes the happy path steps (`authorize` + `capture`) since those already ran. The dispute-only overhead is 665,001 gas (\< \$0.02 on Base).
`requestRefund()` at 418,174 gas is the most expensive operation because it writes to **five storage mappings** for indexing:
* Refund request data (status, amount, payment hash)
* Payer index (`payerRefundRequests[payer][n]`)
* Receiver index (`receiverRefundRequests[receiver][n]`)
* Operator index (`operatorRefundRequests[operator][n]`)
* Counter increments for each index
This indexing enables efficient off-chain queries but costs more gas upfront. On Base, this is still \< \$0.01.
## Summary
| Scenario | Gas | vs transfer | Cost on Base |
| ----------------------------------------------------- | ----------- | ----------- | ------------- |
| ERC-20 transfer (cold baseline) | 10,263 | 1x | \< \$0.001 |
| PaymentOperator, no plugins (`authorize` + `capture`) | 197,092 | 19.2x | \< \$0.005 |
| + fees | 263,771 | 25.7x | \< \$0.005 |
| + fees + simple condition | 268,267 | 26.1x | \< \$0.005 |
| **+ fees + EscrowPeriod + Freeze (x402r full)** | **332,489** | **32.4x** | **\< \$0.01** |
| x402r dispute (off-chain optimized) | 114,778 | 11.2x | \< \$0.005 |
| x402r dispute (fully on-chain, 7 txns) | 992,995 | 96.7x | \< \$0.05 |
The full x402r happy path uses \~32x the gas of a single ERC-20 transfer, but on Base L2, the absolute cost stays under a penny. The overhead comes from escrow validation, fee locking, cross-contract storage writes, and condition checks, all detailed in the per-plugin breakdown above.
All numbers above assume one payment per transaction. Batching operations in a single transaction (via a multicall contract) can reduce per-payment costs by 37 to 80 percent thanks to warm EVM access; contract addresses and shared storage only load once. The benchmark test includes warm measurements for reference.
## Reproducing these numbers
```bash theme={null}
git clone https://github.com/BackTrackCo/x402r-contracts
cd x402r-contracts
git checkout bb188dbc0251f9a3af7da57906d5c59e2b2a14d0
forge test --match-path test/gas/GasBenchmark.t.sol -vv
```
Each test logs its measured gas via `console.log` and the `--gas-report` summary tables list aggregate per-function statistics.
A scheduled CI job in [x402r-contracts](https://github.com/BackTrackCo/x402r-contracts) re-runs `GasBenchmark.t.sol` and flags drift past threshold, so the numbers on this page are checked against the live benchmark rather than left to manual regen. When the benchmark moves, the pinned commit and figures above are refreshed.
## Reference: upstream escrow costs
The escrow layer these numbers build on is Base's audited [Commerce Payments Protocol](https://github.com/base/commerce-payments). For the baseline cost of the underlying `AuthCaptureEscrow` lifecycle (`authorize`, `capture`, `void`, `refund`) independent of x402r's condition and hook plugins, see the upstream contracts and their own benchmarks in the [commerce-payments repository](https://github.com/base/commerce-payments). The x402r figures above are the upstream escrow cost plus the per-plugin overhead detailed in the breakdown.
How the operator calculates and distributes protocol and operator fees
How conditions, hooks, and escrow fit together
# AuthorizationTimeRecorderHook
Source: https://docs.x402r.org/contracts/hooks/authorization-time
Records authorization timestamp for time-based conditions
## Overview
AuthorizationTimeRecorderHook stores `block.timestamp` at the moment the operator authorizes a payment. Time-based conditions like [EscrowPeriod](/contracts/conditions/escrow-period) read this timestamp to gate later actions.
[EscrowPeriod](/contracts/conditions/escrow-period) **extends** AuthorizationTimeRecorderHook and adds an `ICondition` implementation. For escrow enforcement, use EscrowPeriod directly instead of deploying AuthorizationTimeRecorderHook on its own.
## State
```solidity theme={null}
mapping(bytes32 paymentInfoHash => uint256 authorizedAt) public authorizationTimes;
```
## Methods
```solidity theme={null}
// Called after authorize()
function run(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 /* amount */,
address /* caller */,
bytes calldata /* data */
) external {
bytes32 hash = _verifyAndHash(paymentInfo);
authorizationTimes[hash] = block.timestamp;
}
// View function
function getAuthorizationTime(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo
) external view returns (uint256) {
return authorizationTimes[escrow.getHash(paymentInfo)];
}
```
`amount`, `caller`, and `data` are unused; they exist to satisfy `IHook.run`.
## When to Use
Use AuthorizationTimeRecorderHook directly only if you need authorization timestamps **without** escrow period enforcement. For most use cases, [EscrowPeriod](/contracts/conditions/escrow-period) is the better choice since it includes this hook plus time-lock condition logic.
## Gas
**Cost:** \~20k gas per `run()` call (one `SSTORE` for the timestamp).
## Next Steps
Combined hook + condition for escrow enforcement.
Index payments for on-chain queries.
# HookCombinator
Source: https://docs.x402r.org/contracts/hooks/combinator
Chain hooks into a single operator slot for composite state tracking
## Overview
HookCombinator chains hooks into one, invoking each in sequence. Each operator slot accepts only one hook address, so use HookCombinator when you need more than one hook on the same action.
## Deployment
Deploy via HookCombinatorFactory:
```typescript theme={null}
const comboAddress = await hookCombinatorFactory.write.deploy([
[escrowPeriodAddress, paymentIndexRecorderHookAddress] // Records auth time + payment index
]);
config.authorizePostActionHook = comboAddress;
```
## Behavior
* The combinator invokes hooks in the order provided
* **If any hook reverts, all revert**: the entire recording is atomic
* Each hook receives the same `paymentInfo`, `amount`, `caller`, and `data` parameters
## Limits
**Max 10 hooks per combinator.** Each extra hook adds \~1k gas overhead for the delegation call.
## Gas
**Cost:** Sum of all individual hook costs + \~1k gas overhead per hook for delegation.
Example: EscrowPeriod (\~20k) + PaymentIndexRecorderHook (\~20k) + \~2k overhead = \~42k gas total.
## Next Steps
Record authorization timestamps.
Index payments for on-chain queries.
# Custom Hooks
Source: https://docs.x402r.org/contracts/hooks/custom
Build your own hook contracts for specialized state tracking
## Overview
You can build custom hooks for specialized tracking beyond what the built-in hooks provide. Use the `IHook` interface and extend `BaseHook` for operator access control.
## `IHook` interface
```solidity theme={null}
interface IHook {
function run(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 amount,
address caller,
bytes calldata data
) external;
}
```
`data` is forwarded verbatim from the action call (signatures, proofs, attestations). Subclasses ignore parameters they do not need.
## Extending BaseHook
Extend `BaseHook` and call `_verifyAndHash(paymentInfo)` to enforce caller and payment-existence checks:
```solidity theme={null}
contract MyHook is BaseHook {
constructor(address escrow, bytes32 authorizedCodehash)
BaseHook(escrow, authorizedCodehash)
{}
function run(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 amount,
address /* caller */,
bytes calldata /* data */
) external override {
bytes32 hash = _verifyAndHash(paymentInfo);
// Your recording logic here, keyed by `hash`
}
}
```
`authorizedCodehash` is the runtime codehash of an optional trusted caller (e.g. `HookCombinator`). Pass `bytes32(0)` to gate solely on `msg.sender == paymentInfo.operator`.
## Example: CaptureCountHook
Tracks the number and total amount of captures per payment:
```solidity theme={null}
contract CaptureCountHook is BaseHook {
mapping(bytes32 => uint256) public captureCount;
mapping(bytes32 => uint256) public totalCaptured;
constructor(address escrow, bytes32 authorizedCodehash)
BaseHook(escrow, authorizedCodehash)
{}
function run(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 amount,
address /* caller */,
bytes calldata /* data */
) external override {
bytes32 hash = _verifyAndHash(paymentInfo);
captureCount[hash]++;
totalCaptured[hash] += amount;
}
function getStats(bytes32 paymentHash)
external view
returns (uint256 count, uint256 total)
{
return (captureCount[paymentHash], totalCaptured[paymentHash]);
}
}
```
## Testing
Test custom hooks with Forge:
```solidity theme={null}
contract CaptureCountHookTest is Test {
CaptureCountHook hook;
function setUp() public {
hook = new CaptureCountHook(address(escrow), bytes32(0));
}
function test_incrementsOnRun() public {
vm.prank(paymentInfo.operator);
hook.run(paymentInfo, 100e6, address(0), "");
bytes32 hash = escrow.getHash(paymentInfo);
(uint256 count, uint256 total) = hook.getStats(hash);
assertEq(count, 1);
assertEq(total, 100e6);
}
function test_tracksMultipleCaptures() public {
vm.startPrank(paymentInfo.operator);
hook.run(paymentInfo, 50e6, address(0), "");
hook.run(paymentInfo, 30e6, address(0), "");
vm.stopPrank();
bytes32 hash = escrow.getHash(paymentInfo);
(uint256 count, uint256 total) = hook.getStats(hash);
assertEq(count, 2);
assertEq(total, 80e6);
}
}
```
## Security Checklist
* [ ] Extends `BaseHook` and uses `_verifyAndHash` for caller and payment-existence checks
* [ ] Returns early instead of reverting on business-logic edge cases (a reverting hook permanently bricks the surrounding action)
* [ ] Gas-efficient storage layout
* [ ] Full test coverage across the public surface
Unlike conditions, hooks **do mutate state**. `BaseHook._verifyAndHash` enforces `msg.sender == paymentInfo.operator` (or matches `AUTHORIZED_CODEHASH`) plus payment existence in escrow. Calling `_verifyAndHash` is mandatory.
## Next Steps
Compare recording strategies.
Build custom condition contracts.
# Hooks Overview
Source: https://docs.x402r.org/contracts/hooks/overview
State recording system for tracking payment lifecycle events
## What are hooks
Hooks are pluggable contracts that update state **after** an action successfully executes on a PaymentOperator. Each operator has **5 hook slots**, one per action:
| Slot | Records after |
| ---------------------------- | -------------------------------------- |
| `AUTHORIZE_POST_ACTION_HOOK` | Authorization (for example, timestamp) |
| `CHARGE_POST_ACTION_HOOK` | Charge event |
| `CAPTURE_POST_ACTION_HOOK` | Capture from escrow |
| `VOID_POST_ACTION_HOOK` | Void |
| `REFUND_POST_ACTION_HOOK` | Refund (after capture) |
These are the hook half of the operator's 10 slots. For the full slot layout alongside the pre-action conditions, see [PaymentOperator: 10-slot configuration](/contracts/payment-operator#10-slot-configuration).
## IHook Interface
```solidity theme={null}
interface IHook {
function run(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 amount,
address caller,
bytes calldata data
) external;
}
```
**Parameters:**
* `paymentInfo`, The payment information struct
* `amount`, The amount involved in the action
* `caller`, The address that executed the action (msg.sender on operator)
* `data`, Arbitrary data forwarded from the caller (signatures, proofs, attestations)
## Default behavior
**Hook slot = `address(0)`**: no-op (does nothing). The operator records no state for that slot.
Set hooks only on the slots where you want state tracking. Leave the rest as `address(0)`.
## BaseHook
All built-in hooks extend `BaseHook`, which verifies that the caller is an authorized operator. This prevents unauthorized contracts from writing state.
## Choosing a recording strategy
Not every payment needs on-chain hooks. Choose based on your use case:
### Events only (\~0 extra gas)
The operator already emits an event for every action (`AuthorizeExecuted`, `CaptureExecuted`, and the same shape for the rest). If you only need payment history for analytics or display, skip hooks entirely and index events off-chain.
**Best for:** micropayments, high-volume payments where gas overhead matters, simple UIs.
### Events plus subgraph (richest queries)
Index operator events with a subgraph for rich queries (payment history by payer, receiver, status, date range). No on-chain hook gas cost.
**Best for:** analytics dashboards, payment history, cross-payment queries.
**Trade-off:** requires subgraph infrastructure (semi-centralized).
### On-chain hooks (\~20k gas per write)
Use hooks when you need **on-chain reads**: other contracts or conditions that depend on recorded state. [EscrowPeriod](/contracts/conditions/escrow-period) is the most common example. It records authorization time so the capture condition can check if the escrow window has passed.
**Best for:** escrow enforcement, dispute evidence, decentralized frontends, on-chain composability.
**Trade-off:** \~20k gas per `SSTORE` operation.
### Decision table
| Need | Strategy | Hook slots |
| ----------------------- | ----------------- | ----------------------------------------------------------------------------------- |
| Payment history for UI | Events only | `address(0)` |
| Rich queries, analytics | Events + Subgraph | `address(0)` |
| Time-locked releases | On-chain | [EscrowPeriod](/contracts/conditions/escrow-period) on `AUTHORIZE_POST_ACTION_HOOK` |
| On-chain payment index | On-chain | [PaymentIndexRecorderHook](/contracts/hooks/payment-index) |
| Many data points | On-chain | [HookCombinator](/contracts/hooks/combinator) |
For most configurations, you only need a hook on the `AUTHORIZE_POST_ACTION_HOOK` slot (for [EscrowPeriod](/contracts/conditions/escrow-period)). Leave other hook slots as `address(0)`.
## Next Steps
Record authorization timestamps.
Index payments for on-chain queries.
Chain hooks into one slot.
Build your own hook.
# PaymentIndexRecorderHook
Source: https://docs.x402r.org/contracts/hooks/payment-index
Index payments by sequential count for on-chain queries and repeated refund requests
## Overview
PaymentIndexRecorderHook indexes payments by payer and receiver and stores the full `PaymentInfo` struct keyed by `paymentInfoHash`. Wire it into `AUTHORIZE_POST_ACTION_HOOK` on a `HookCombinator` (or directly when authorize is the only hook slot you use) so each new authorization registers itself for on-chain lookups.
## When to use
* You need on-chain payment lookups **without a subgraph**
* Other contracts need to read the full `PaymentInfo` for a hash, or page through every payment by payer / receiver
* You want a single chain-singleton index that aggregates across every operator routing through `HookCombinator`
**Skip when:** you're using a subgraph for payment queries, since the subgraph can derive indexes from events without the on-chain gas cost.
## State
```solidity theme={null}
mapping(bytes32 paymentInfoHash => AuthCaptureEscrow.PaymentInfo) private paymentInfoStore;
mapping(address payer => mapping(uint256 index => bytes32 hash)) private payerPayments;
mapping(address payer => uint256 count) public payerPaymentCount;
mapping(address receiver => mapping(uint256 index => bytes32 hash)) private receiverPayments;
mapping(address receiver => uint256 count) public receiverPaymentCount;
```
## Methods
```solidity theme={null}
function run(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 /* amount */,
address /* caller */,
bytes calldata /* data */
) external {
bytes32 hash = escrow.getHash(paymentInfo);
paymentInfoStore[hash] = paymentInfo;
payerPayments[paymentInfo.payer][payerPaymentCount[paymentInfo.payer]++] = hash;
receiverPayments[paymentInfo.receiver][receiverPaymentCount[paymentInfo.receiver]++] = hash;
}
function getPaymentInfo(
bytes32 paymentInfoHash
) external view returns (AuthCaptureEscrow.PaymentInfo memory);
function getPayerPayments(
address payer,
uint256 offset,
uint256 count
) external view returns (AuthCaptureEscrow.PaymentInfo[] memory, uint256 total);
function getReceiverPayments(
address receiver,
uint256 offset,
uint256 count
) external view returns (AuthCaptureEscrow.PaymentInfo[] memory, uint256 total);
```
`amount`, `caller`, and `data` are unused; they exist to satisfy `IHook.run`.
## Querying
```typescript theme={null}
const info = await paymentIndex.read.getPaymentInfo([paymentInfoHash])
const [payerInfos, total] = await paymentIndex.read.getPayerPayments([
payer,
0n,
10n,
])
```
## Gas
**Cost:** \~175k gas per authorization (payer index + receiver index + full `PaymentInfo` SSTORE).
## Next Steps
Combine with other hooks in a single slot.
Compare recording strategies.
# RefundRequest
Source: https://docs.x402r.org/contracts/hooks/refund-request
Manages refund request lifecycle and approvals independent of operator implementation
## Overview
* **Type:** Singleton (one per network)
* **Deployment:** Direct deployment (no factory)
* **Purpose:** Track refund request lifecycle
## Request Types
**Who can request:** Payer, Receiver, OR Arbiter
**Typical flow:**
1. Payer suspects fraud, requests a refund
2. Arbiter investigates
3. Arbiter approves or denies the request
4. If approved, arbiter calls `operator.void()`
**Use cases:**
* Buyer remorse
* Seller fraud
* Payment error
**Who can request:** Receiver only
**Typical flow:**
1. Receiver realizes product defect after capture
2. Receiver requests a refund
3. Arbiter investigates
4. If approved, arbiter calls `operator.refund()`
**Use cases:**
* Product defects discovered later
* Service not as described
* Voluntary refund by merchant
## Request status states
Each payment supports one refund request, keyed by `paymentInfoHash`. The payer may only request again after cancelling the prior request.
```mermaid theme={null}
stateDiagram-v2
[*] --> Pending: payer requestRefund()
Pending --> Approved: arbiter (via operator hook on capture/void/refund)
Pending --> Denied: arbiter deny()
Pending --> Refused: arbiter refuse()
Pending --> Cancelled: payer cancelRefundRequest()
Cancelled --> Pending: payer requestRefund() again
note right of Approved
Status flips to Approved automatically
when the arbiter executes the refund
via operator.void() / operator.refund()
(wired through VOID_POST_ACTION_HOOK).
end note
```
## Key methods
### requestRefund()
Creates a refund request for this payment. Only the payer can call.
```solidity theme={null}
function requestRefund(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint120 amount
) external
```
**Parameters:**
* `paymentInfo`: PaymentInfo struct
* `amount`: refund amount the payer is asking for (uint120)
**Reverts** if a non-cancelled request already exists, if the payment is unknown to the canonical escrow, or if `amount == 0`.
### cancelRefundRequest()
Payer cancels their own pending request, freeing the slot to request again.
```solidity theme={null}
function cancelRefundRequest(AuthCaptureEscrow.PaymentInfo calldata paymentInfo) external
```
**Access:** only the payer.
### deny()
Arbiter rejects the claim after reviewing evidence. Terminal state.
```solidity theme={null}
function deny(AuthCaptureEscrow.PaymentInfo calldata paymentInfo) external
```
### refuse()
Arbiter refuses to consider the request (spam, out of jurisdiction, invalid). Terminal state.
```solidity theme={null}
function refuse(AuthCaptureEscrow.PaymentInfo calldata paymentInfo) external
```
### Approval
The contract has no `updateStatus` or explicit `approve` entrypoint. Approval happens automatically when the arbiter executes the refund through the operator (`operator.void()` or `operator.refund()`), which fires `VOID_POST_ACTION_HOOK` / `REFUND_POST_ACTION_HOOK` and flips status to `Approved`.
### Query helpers
* `getRefundRequest(paymentInfo)`: full `RefundRequestData` for this payment
* `hasRefundRequest(paymentInfo)`: boolean
* `getRefundRequestStatus(paymentInfo)`: just the `RequestStatus`
* `getPayerRefundRequests(payer, offset, count)` / `getReceiverRefundRequests(...)` / `getOperatorRefundRequests(...)`: paginated index lookups
## Usage example
```typescript theme={null}
// 1. Payer files refund request
await refundRequest.write.requestRefund([paymentInfo, requestedAmount])
// Status: Pending
// 2a. Arbiter denies (terminal):
await refundRequest.write.deny([paymentInfo])
// 2b. Arbiter approves by executing the refund via the operator.
// The post-action hook auto-flips the request to Approved.
// Before capture:
await operator.write.void([paymentInfo, '0x'])
// After capture:
await operator.write.refund([paymentInfo, amount, tokenCollector, collectorData])
```
RefundRequest is the request-tracking layer. Executing the refund is always a separate call into the operator/escrow.
# License
Source: https://docs.x402r.org/contracts/license
BUSL-1.1 licensing terms, permissions, and restrictions for x402r smart contracts
## Why BUSL-1.1
The code is fully readable and usable on-chain. You can integrate with deployed x402r contracts, build on top of them, and inspect every line of source. The license protects against forks that compete with or commoditize the protocol (for example, stripping fees and redeploying), so that protocol fees can keep funding development, audits, and new features for everyone building on x402r.
BUSL-1.1 protects against that while keeping the code open. After the Change Date, everything converts to MIT and is fully permissionless. Uniswap, Aave, and other major DeFi protocols use the same approach.
## License Terms
The **Business Source License 1.1** covers all Solidity source files in `x402r-contracts/src/`.
| Parameter | Value |
| ------------------- | -------------------------------------- |
| **License** | [BUSL-1.1](https://mariadb.com/bsl11/) |
| **SPDX Identifier** | `BUSL-1.1` |
| **Licensor** | Ali Abdoli and Vrajang Parikh |
| **Licensed Work** | x402r-contracts |
| **Copyright** | 2025-2026 |
| **Change Date** | December 9, 2029 |
| **Change License** | MIT License |
## What you can do
### On-chain
You can interact with deployed x402r contracts:
* **Integrate**: call x402r contracts from your own contracts or dApps
* **Build**: create applications, services, and protocols on top of x402r
* **Deploy via factories**: use x402r's official factories (for example, `PaymentOperatorFactory` and `EscrowPeriodFactory`) to deploy your own operator instances with your own configuration
### Off-chain
You can work with the source code:
* **Read and learn** from the code
* **Fork and adapt** for local development and testing
* **Redistribute** the source code
* **Deploy locally**: spin up Anvil, Hardhat, or any local or test environment for integration testing
### The one restriction
Do not deploy x402r contracts outside of the official factories, whether modified or unmodified.
Deploying through x402r's factories is the intended path and is always allowed. What you cannot do:
* Take the source code and deploy your own instances outside of the factories
* Deploy a modified fork (for example, removing fees or changing parameters) to any production chain
* Remove or change the license notice
Deploying to local chains, testnets, and private forks for **development and testing** is fine.
### Change Date
On **December 9, 2029** (or 4 years after the first public release of each version, whichever comes first), the license automatically converts to the **MIT License**, at which point you can deploy, fork, and do anything you want.
## Summary
| | Now | After Dec 9, 2029 |
| ---------------------------- | ----------- | ----------------- |
| **License** | BUSL-1.1 | MIT |
| **Integrate on-chain** | Free | Free |
| **Build on top** | Free | Free |
| **Deploy via factories** | Free | Free |
| **Deploy to local/testnet** | Free | Free |
| **Deploy outside factories** | Not allowed | Free |
# Overview
Source: https://docs.x402r.org/contracts/overview
Introduction to x402r smart contracts and their relationship to commerce-payments
## What is x402r
x402r builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) to add dispute resolution, escrow periods, and refund capabilities. For the protocol-level introduction and the payer/merchant/arbiter model, see [What is x402r](/). This page covers the contract layer.
## Architecture Layers
See the [system architecture diagrams](https://github.com/BackTrackCo/x402r-contracts#architecture) in the x402r-contracts repository.
## Commerce Payments (Base Layer)
The audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) provides the foundational payment infrastructure. x402r uses the canonical contracts directly (no fork) at their universal CREATE2 addresses.
### AuthCaptureEscrow
Core escrow contract for holding ERC-20 tokens during payments.
**Features:**
* Authorization-based deposits (no direct transfers)
* Per-payment state queried via `paymentState(hash)` → `(hasCollected, capturableAmount, ...)`
* Void/reclaim for failed authorizations
* CaptureAuthorizer-based access control
**Key Methods:**
```solidity theme={null}
authorize(paymentInfo, amount, tokenCollector, collectorData)
charge(paymentInfo, amount, tokenCollector, collectorData)
capture(paymentInfo, amount, data)
void(paymentInfo, data)
reclaim(paymentInfo, data)
```
### ERC3009PaymentCollector
Payment collection with ERC-3009 transferWithAuthorization support.
**Features:**
* Gasless payments via meta-transactions
* Nonce-based replay protection
* Deadline-based expiry
* Integration with Multicall3 for batching
### TokenStore
Safe token storage and transfer utilities.
**Features:**
* Reentrancy-safe transfers
* Balance tracking
* Integration with OpenZeppelin SafeERC20
## What x402r Adds
x402r extends commerce-payments with flexible payment capabilities:
### 1. Generic Payment Operator
**PaymentOperator** - Flexible payment operator with pluggable conditions
**RefundRequest** - Structured refund request workflow
**Key additions:**
* Fee recipient for protocol and operator fee distribution
* Configurable authorization via conditions (not hardcoded roles)
* Refund request states: `Pending` → `Approved`/`Denied`/`Cancelled`
* Voids (during escrow period)
* Refunds (after capture)
* Support for marketplace, subscription, streaming, and custom flows
### 2. Pluggable Condition System
**Conditions (ICondition)** - Authorization checks before actions
**Hooks (IHook)** - State updates after actions
**10-slot configuration per operator:**
* 5 condition slots (before action): `authorize`, `charge`, `capture`, `void`, `refund`
* 5 hook slots (after action): state tracking for each action
**Benefits:**
* Configure operator behavior without redeploying
* Compose complex authorization logic with combinators
* Reuse condition contracts across operators
* Gas-efficient with shared singletons
### 3. Time-Based Escrow & Freeze Policies
**EscrowPeriod** - Combined hook and condition that tracks authorization time and enforces escrow period
**Freeze** - Standalone condition that blocks capture while a freeze remains active (with configurable freeze/unfreeze authorization)
**Key features:**
* Configurable escrow periods (for example, 7 days or 14 days)
* Payer-initiated freezes to stop suspicious releases
* Time-limited freeze durations (for example, 3 days)
* MEV protection via private mempool support
* Composable via `AndCondition([escrowPeriod, freeze])`
### 4. Arbiter & Evidence System
**RefundRequestEvidence** - On-chain evidence submission with IPFS CIDs and arbiter EIP-712 signature approval
### 5. Factory Pattern
**PaymentOperatorFactory** - Deploys operators with deterministic CREATE2 addresses
**EscrowPeriodFactory** - Deploys EscrowPeriod contracts
**FreezeFactory** - Deploys Freeze condition contracts
Plus factories for: StaticFeeCalculator, StaticAddressCondition, AndCondition, OrCondition, NotCondition, HookCombinator.
All factories use **universal CREATE2 addresses**: same address on every supported chain.
**Benefits:**
* Predictable addresses for off-chain address generation
* Cross-chain address consistency (same config = same address on every chain)
* Shared configuration reduces deployment costs
* Idempotent deployments (same config = same address)
* Owner controls all deployed instances via factory
## Key Differences from Commerce Payments
| Feature | Commerce Payments | x402r |
| ---------------------- | -------------------- | ----------------------------------------------------------------- |
| **Refunds** | Manual void/reclaim | Structured refund requests with configurable approval |
| **Escrow Period** | Not enforced | Configurable time-lock before capture |
| **Dispute Resolution** | Not built-in | Arbiter workflow via conditions, signatures, and evidence |
| **Authorization** | Operator-based only | Pluggable conditions (access, time, signature, combinators) |
| **Freeze Mechanism** | Not available | Configurable freeze during escrow period |
| **Deployment** | Direct deployment | Factory pattern with universal CREATE2 (same address every chain) |
| **Fees** | Not enforced | Additive protocol + operator fees with 7-day timelock |
| **Multi-chain** | Per-chain deployment | Universal CREATE2 addresses on supported chains |
## Use Cases
### Commerce Payments (Base) Suitable For:
* Simple payment escrow
* Trusted operator scenarios
* No refund requirements
* Direct integrations
### x402r (Extension) Suitable For:
* **Marketplaces** - Buyer protection with time-based escrow and disputes
* **Subscriptions** - Time-limited authorizations with charge capability
* **Streaming** - Time-proportional payments with custom conditions
* **Grants/DAO** - Multisig-controlled releases and refunds
* **Escrow Services** - Professional escrow with freeze protection
* **API Payments** - Service provider controlled charge flows
## Design Principles
Most contracts are immutable to prevent rug pulls and ensure trustlessness:
* **PaymentOperator** - Cannot pause or upgrade
* **EscrowPeriod** - Cannot change escrow period
* **Freeze** - Cannot change freeze rules after deployment
Protocol fee configuration is mutable via `ProtocolFeeConfig` (with 7-day timelock). Operator fees are immutable.
* Condition singletons deployed once, reused everywhere
* CREATE2 for deterministic addresses (no registry lookups)
* Minimal storage in operators (conditions are stateless)
* Hook pattern separates state from logic
* Conditions compose with And/Or/Not logic
* Operators can share condition implementations
* Factories enable on-demand instance deployment
* Stateless conditions work across many operators
* Reentrancy guards on all state changes
* 7-day timelock on protocol fee changes
* Two-step ownership transfers
* Detailed event logging for monitoring
## Architecture Overview
For a detailed view of how all contracts interact, see [Architecture](/contracts/architecture).
To understand the core operator, see [PaymentOperator](/contracts/payment-operator). For supporting contracts, see [Periphery](/contracts/periphery/overview).
For factory deployment patterns, see [Factories](/contracts/factories).
## Next Steps
View system architecture diagrams and payment flows.
Learn about the core operator contract.
Deploy a PaymentOperator using the SDK.
Build with the TypeScript SDK.
# PaymentOperator
Source: https://docs.x402r.org/contracts/payment-operator
The core payment operator contract with pluggable conditions and fee management
## Overview
The main payment operator contract with pluggable conditions for flexible authorization logic.
* **Type:** Operator instance (one per fee recipient + configuration)
* **Deployment:** Via PaymentOperatorFactory
* **Immutability:** No pause switch, no upgrade path
* **Configuration:** 10 slots for conditions and hooks
* **Use Cases:** Marketplace, subscriptions, streaming, grants, custom flows
## Immutable Fields
```solidity theme={null}
address public immutable ESCROW; // AuthCaptureEscrow address
address public immutable FEE_RECEIVER; // Operator fee recipient
ProtocolFeeConfig public immutable PROTOCOL_FEE_CONFIG; // Shared protocol fee config
IFeeCalculator public immutable FEE_CALCULATOR; // Operator fee calculator
```
## State
```solidity theme={null}
// Fee tracking for accurate distribution
mapping(address token => uint256) public accumulatedProtocolFees;
// Fees locked at authorization time
mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees;
```
## 10-Slot Configuration
1. **AUTHORIZE\_PRE\_ACTION\_CONDITION** - Who can authorize payments
2. **CHARGE\_PRE\_ACTION\_CONDITION** - Who can charge partial amounts
3. **CAPTURE\_PRE\_ACTION\_CONDITION** - Who can capture funds from escrow
4. **VOID\_PRE\_ACTION\_CONDITION** - Who can refund during escrow
5. **REFUND\_PRE\_ACTION\_CONDITION** - Who can refund after capture
**Default:** `address(0)` = always allow
1. **AUTHORIZE\_POST\_ACTION\_HOOK** - Record authorization (for example, timestamp)
2. **CHARGE\_POST\_ACTION\_HOOK** - Record charge event
3. **CAPTURE\_POST\_ACTION\_HOOK** - Record capture
4. **VOID\_POST\_ACTION\_HOOK** - Record void
5. **REFUND\_POST\_ACTION\_HOOK** - Record refund
**Default:** `address(0)` = no recording (no-op)
## Key Methods
### authorize()
Authorizes a payment and locks funds in escrow.
```solidity theme={null}
function authorize(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 amount,
address tokenCollector,
bytes calldata collectorData
) external nonReentrant
```
**Parameters:**
* `paymentInfo` - Payment info struct (must have `operator == address(this)`, `feeReceiver == address(this)`)
* `amount` - Amount to authorize
* `tokenCollector` - Address of the token collector contract
* `collectorData` - Data to pass to the token collector
**Flow:**
1. Check `AUTHORIZE_PRE_ACTION_CONDITION` (if set)
2. Check fee bounds compatibility
3. Store fees at authorization time (prevents protocol fee changes from breaking capture)
4. Call `escrow.authorize()`
5. Call `AUTHORIZE_POST_ACTION_HOOK` (if set)
6. Emit `AuthorizeExecuted`
**Access:** Controlled by `AUTHORIZE_PRE_ACTION_CONDITION` (default: anyone)
**Authorization Expiry:** The `PaymentInfo` struct includes an `authorizationExpiry` field (from base commerce-payments). Set this to `type(uint48).max` for no expiry, or specify a timestamp to let the payer reclaim funds after expiry. Subscription-based payments use this to bound the authorization window.
### charge()
Direct charge - collects payment and immediately transfers to receiver (no escrow hold).
```solidity theme={null}
function charge(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 amount,
address tokenCollector,
bytes calldata collectorData
) external nonReentrant
```
**Parameters:**
* `paymentInfo` - Payment info struct (must have `operator == address(this)`, `feeReceiver == address(this)`)
* `amount` - Amount to charge
* `tokenCollector` - Address of the token collector contract
* `collectorData` - Data to pass to the token collector
**Flow:**
1. Check `CHARGE_PRE_ACTION_CONDITION` (if set)
2. Check fee bounds compatibility
3. Call `escrow.charge()` - funds go directly to receiver
4. Accumulate protocol fees for later distribution
5. Call `CHARGE_POST_ACTION_HOOK` (if set)
6. Emit `ChargeExecuted`
**Access:** Controlled by `CHARGE_PRE_ACTION_CONDITION` (default: anyone)
Unlike `authorize()`, funds go directly to receiver without escrow hold. Refunds are only possible via `refund()`.
### capture()
Releases funds from escrow to receiver (capture).
```solidity theme={null}
function capture(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 amount,
bytes calldata data
) external nonReentrant
```
**Parameters:**
* `paymentInfo` - Payment info struct
* `amount` - Amount to capture
* `data` - Optional pass-through data for the pre/post action plugins
**Flow:**
1. Check `CAPTURE_PRE_ACTION_CONDITION` (if set)
2. Use fees stored at authorization time
3. Call `escrow.capture()`
4. Accumulate protocol fees for later distribution
5. Call `CAPTURE_POST_ACTION_HOOK` (if set)
6. Emit `CaptureExecuted`
**Access:** Controlled by `CAPTURE_PRE_ACTION_CONDITION`
**Marketplace example:** Receiver OR StaticAddressCondition(arbiter) + escrow passed
**Subscription example:** StaticAddressCondition(serviceProvider)
**DAO example:** StaticAddressCondition(daoMultisig)
### void()
Returns all escrowed funds to the payer before capture. Full-only: `escrow.void()` empties the authorization in one transaction.
```solidity theme={null}
function void(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
bytes calldata data
) external nonReentrant
```
**Parameters:**
* `paymentInfo` - Payment info struct
* `data` - Optional pass-through data for the pre/post action plugins
**Flow:**
1. Check `VOID_PRE_ACTION_CONDITION` (if set)
2. Call `escrow.void()` to return escrowed funds to payer
3. Call `VOID_POST_ACTION_HOOK` (if set)
4. Emit `VoidExecuted`
**Access:** Controlled by `VOID_PRE_ACTION_CONDITION`
**Marketplace example:** StaticAddressCondition(arbiter) for disputes
**Return policy example:** Receiver OR StaticAddressCondition(arbiter)
**DAO example:** StaticAddressCondition(daoMultisig)
**Subscription example:** address(0), no voids allowed
For a partial return, call `capture()` for the amount to keep. Then `void()` the unused authorization, or let the payer reclaim it after `captureDeadline`.
### refund()
Refunds a payment after capture (after the receiver has the funds).
```solidity theme={null}
function refund(
AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
uint256 amount,
address tokenCollector,
bytes calldata collectorData
) external nonReentrant
```
**Parameters:**
* `paymentInfo` - Payment info struct
* `amount` - Amount to refund to payer
* `tokenCollector` - Address of the token collector that will source the refund
* `collectorData` - Data to pass to the token collector (for example, signatures)
**Flow:**
1. Check `REFUND_PRE_ACTION_CONDITION` (if set)
2. Call `escrow.refund()` - token collector enforces permission
3. Call `REFUND_POST_ACTION_HOOK` (if set)
4. Emit `RefundExecuted`
**Access:** Controlled by `REFUND_PRE_ACTION_CONDITION`. The token collector also enforces permission (for example, the receiver must have approved it, or `collectorData` contains the receiver's signature).
**Marketplace example:** StaticAddressCondition(arbiter) - post-delivery disputes
**Return policy example:** Receiver - voluntary returns
**Most configurations:** address(0) - no refunds
## Fee System (Modular, Additive)
Fees are additive and modular: `totalFee = protocolFee + operatorFee`
### Fee Architecture
```solidity theme={null}
// Shared protocol fee config (timelocked, swappable calculator)
ProtocolFeeConfig public immutable PROTOCOL_FEE_CONFIG;
// Per-operator fee calculator (immutable, set at deploy)
IFeeCalculator public immutable FEE_CALCULATOR;
// Fee recipients
address public immutable FEE_RECEIVER; // Operator fee recipient
// Protocol fee recipient is on ProtocolFeeConfig
// Fee tracking for accurate distribution
mapping(address token => uint256) public accumulatedProtocolFees;
```
Fees are additive: `totalFee = protocolFee + operatorFee`, split between `protocolFeeRecipient` and the operator's `FEE_RECEIVER`. For a worked example with concrete amounts, see the [Fee System](/contracts/fees#example-calculation).
**Fee Locking:**
The operator calculates fees at `authorize()` time and stores them in `authorizedFees[hash]`. This stops later protocol fee changes from breaking capture of already-authorized payments.
```solidity theme={null}
struct AuthorizedFees {
uint16 totalFeeBps;
uint16 protocolFeeBps;
}
mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees;
```
**FEE\_RECEIVER** can be:
* Arbiter (marketplace with disputes)
* Service Provider (subscriptions)
* Platform Treasury (platform-controlled)
* DAO Multisig (governance-controlled)
### Fee Distribution
Fees accumulate in the operator contract. Call `distributeFees(token)` to disburse them:
```solidity theme={null}
// Anyone can call to distribute fees for a token
operator.distributeFees(usdcAddress);
// Protocol share -> protocolFeeRecipient
// Operator share -> FEE_RECEIVER
```
### Protocol Fee Changes (7-day Timelock)
Protocol fee calculator and recipient changes require a 7-day timelock on `ProtocolFeeConfig`. See [Fee System: 7-day timelock](/contracts/fees#calculator-changes-7-day-timelock) for the full queue, wait, execute workflow.
Protocol fee changes require 7-day timelock. Operator fees are immutable (set at deploy time).
## 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 stay fixed after deployment
## Next Steps
AuthCaptureEscrow, RefundRequest, and other supporting contracts.
Explore the pluggable condition system.
Deploy operators with factory patterns.
See real-world configuration examples.
# Commerce Payments
Source: https://docs.x402r.org/contracts/periphery/auth-capture-escrow
AuthCaptureEscrow and the canonical token collectors that form the auth-capture base layer
x402r builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) (no fork). Three contracts from this stack form the base layer:
* **AuthCaptureEscrow**: singleton escrow that holds funds and gates lifecycle actions on the `captureAuthorizer` (committed on-chain as `PaymentInfo.operator`).
* **ERC3009PaymentCollector**: collects funds via signed ERC-3009 `receiveWithAuthorization`.
* **Permit2PaymentCollector**: collects funds via Uniswap Permit2 `permitTransferFrom`.
All three sit at universal CREATE2 addresses (same address on every supported chain).
| Contract | Canonical address |
| ------------------------- | -------------------------------------------- |
| `AuthCaptureEscrow` | `0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff` |
| `ERC3009PaymentCollector` | `0x0E3dF9510de65469C4518D7843919c0b8C7A7757` |
| `Permit2PaymentCollector` | `0x992476B9Ee81d52a5BdA0622C333938D0Af0aB26` |
## AuthCaptureEscrow
Core escrow contract for holding ERC-20 tokens during the payment lifecycle.
### Payment State Machine
```mermaid theme={null}
stateDiagram-v2
[*] --> NonExistent
NonExistent --> InEscrow: authorize()
InEscrow --> Captured: capture()
InEscrow --> Settled: void()
Captured --> Settled: reclaim() / refund()
Settled --> [*]
note right of InEscrow
Funds locked in escrow.
Payer can reclaim after captureDeadline.
end note
note right of Captured
Funds transferred to receiver.
Can still refund within refundDeadline.
end note
note right of Settled
Terminal state.
No further actions possible.
end note
```
### Key methods
#### authorize()
Pulls funds into escrow via the token collector. Only the `captureAuthorizer` (typically a facilitator EOA, or a smart contract acting as captureAuthorizer) can call it.
```solidity theme={null}
function authorize(
PaymentInfo calldata paymentInfo,
uint256 amount,
address tokenCollector,
bytes calldata collectorData
) external
```
`tokenCollector` is `ERC3009PaymentCollector` or `Permit2PaymentCollector` depending on `assetTransferMethod` in the scheme `extra`. `collectorData` carries the raw ERC-3009 signature or the ABI-encoded Permit2 signature.
#### charge()
Single-shot atomic settlement: pulls funds and transfers directly to the receiver, no escrow hold.
```solidity theme={null}
function charge(
PaymentInfo calldata paymentInfo,
uint256 amount,
address tokenCollector,
bytes calldata collectorData
) external
```
#### capture()
Releases escrowed funds to the receiver, minus fees.
```solidity theme={null}
function capture(
PaymentInfo calldata paymentInfo,
uint256 amount,
uint16 feeBps,
address feeReceiver
) external
```
#### void()
Returns all escrowed funds to the payer. Full-only: `void()` empties the authorization in one transaction.
```solidity theme={null}
function void(PaymentInfo calldata paymentInfo) external
```
#### reclaim()
Payer-only: gated by `onlySender(paymentInfo.payer)`. The payer can pull funds back out of escrow after `captureDeadline` if the captureAuthorizer never captured. No third party (including the operator or arbiter) can call `reclaim` on the payer's behalf.
```solidity theme={null}
function reclaim(PaymentInfo calldata paymentInfo) external
```
#### refund()
Returns funds to the payer after capture, sourced via a token collector (typically pulled from the merchant's balance).
```solidity theme={null}
function refund(
PaymentInfo calldata paymentInfo,
uint256 amount,
address tokenCollector,
bytes calldata collectorData
) external
```
### Access control
Lifecycle actions (`authorize`, `charge`, `capture`, `void`, `refund`) check `msg.sender` against `PaymentInfo.operator` (the captureAuthorizer). Anyone can call `reclaim` after `captureDeadline`.
The escrow has no global "operator whitelist." Access is per-payment, governed by the signed `PaymentInfo`.
### Security features
* **Replay prevention**: each payment has a unique nonce derived from `(chainId, escrowAddress, paymentInfoHash)`, consumed on-chain at settlement
* **Fee bounds enforcement**: the client signs `minFeeBps` / `maxFeeBps` / `feeReceiver` in `PaymentInfo`; the escrow rejects out-of-bounds captures/charges
* **Expiry ordering**: contract enforces `preApprovalExpiry <= authorizationExpiry <= refundExpiry`
* **Reentrancy protection** on all state-changing entry points
***
## ERC3009PaymentCollector
Collects ERC-20 tokens into escrow using the client's off-chain ERC-3009 signature. The payer never submits a transaction.
### How it works
The escrow calls the token collector during `authorize()` or `charge()`, passing the client's signature as `collectorData`. The collector executes `receiveWithAuthorization` (ERC-3009) to pull tokens from the payer.
### Features
* **ERC-3009 `receiveWithAuthorization()`**: gasless token transfers via signed messages
* **EIP-6492 support**: handles smart wallet clients with deployment bytecode in signatures (via `ERC6492SignatureHandler`)
* **Nonce-based replay protection**: each authorization can run only once; the nonce is the payer-agnostic `PaymentInfo` hash
* **Deadline-based expiry**: `validBefore` (typically `now + maxTimeoutSeconds`) blocks stale authorizations
### ERC-3009 signature
The client signs an EIP-712 typed data message with primary type `ReceiveWithAuthorization`:
```typescript theme={null}
const authorization = {
from: payerAddress, // Who is paying
to: tokenCollectorAddress, // ERC3009PaymentCollector (canonical)
value: amount, // Amount in token decimals
validAfter: 0, // Earliest valid time (0 = immediately)
validBefore: deadline, // Latest valid time
nonce: derivedNonce, // Payer-agnostic PaymentInfo hash
}
```
The auth-capture scheme uses `receiveWithAuthorization` (not `transferWithAuthorization`). The token collector is the `to` address, which then routes tokens into the escrow.
***
## Permit2PaymentCollector
Collects ERC-20 tokens through Uniswap Permit2 `permitTransferFrom`. The operator selects this collector when `assetTransferMethod === "permit2"` in the scheme `extra`. Any ERC-20 the payer has approved Permit2 for becomes spendable through this collector.
The client signs a Permit2 `PermitTransferFrom`; the deterministic nonce binds the merchant address, removing the need for a separate witness struct.
See the [auth-capture wire format](/x402-integration/auth-capture/wire-format) for the full Permit2 wire format.
# Periphery Overview
Source: https://docs.x402r.org/contracts/periphery/overview
Supporting contracts that extend the PaymentOperator: escrow, refund requests, token collectors, and more
## What are periphery contracts
Periphery contracts support the [PaymentOperator](/contracts/payment-operator) without being the operator itself. They handle escrow storage, token collection, refund workflows, and evidence submission.
## Contract Map
| Contract | Role | Type |
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------- |
| [Commerce Payments](/contracts/periphery/auth-capture-escrow) | AuthCaptureEscrow + ERC3009PaymentCollector + Permit2PaymentCollector (base layer) | Singleton |
| [RefundRequestEvidence](/contracts/periphery/refund-request-evidence) | On-chain evidence submission tied to RefundRequest | Singleton |
| [ReceiverRefundCollector](/contracts/periphery/receiver-refund-collector) | Pulls funds from receiver for refunds (after capture) | Singleton |
**RefundRequest** is a hook plugin, see [Hooks: RefundRequest](/contracts/hooks/refund-request).
## Contract Addresses
All periphery contracts use **universal CREATE2 addresses**: the same address on every supported chain.
| Contract | Address |
| ----------------------- | -------------------------------------------- |
| AuthCaptureEscrow | `0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff` |
| ERC3009PaymentCollector | `0x0E3dF9510de65469C4518D7843919c0b8C7A7757` |
| Permit2PaymentCollector | `0x992476B9Ee81d52a5BdA0622C333938D0Af0aB26` |
| ProtocolFeeConfig | `0xBe2d24614F339a1eB103A399F93AA2a39Ca815Bc` |
| ReceiverRefundCollector | `0x88C9826dFA17Ad9d3a726015C667dD995394D341` |
| RefundRequestEvidence | `0x4089A5A853e9eF35f504B842795fB272dF69c739` |
### Factories
| Factory | Address |
| ----------------------------- | -------------------------------------------- |
| PaymentOperatorFactory | `0xa0d4734842df1690a5B33Cb21828c946e39D55a2` |
| EscrowPeriodFactory | `0xe72D2014ebC48F1d92521e8629574918E8030548` |
| FreezeFactory | `0xeC092cf1215DB44af0Abe87c1157E304FEa5d0Eb` |
| StaticFeeCalculatorFactory | `0x97F99AB01F86b480f751B7b81166Dbe1F113e6C3` |
| StaticAddressConditionFactory | `0x77B379390750E1d3F802cC220926694D2454903E` |
| AndConditionFactory | `0x2B07d750C639b65a26e43F1FDCE404b21DCf16D9` |
| OrConditionFactory | `0x0519a37c0A996DD5F1e81e07b4aD3B24C257BC90` |
| NotConditionFactory | `0xb9c3223D059C3cAbD482bB54f3d7cD52DE70A9ae` |
| HookCombinatorFactory | `0x30B5373FD791D2d7b28C3B8020EB68b032f3f960` |
### Condition Singletons
| Condition | Address |
| ------------------- | -------------------------------------------- |
| PayerCondition | `0x586486394C38A2a7d36B16a3FDaF366cd202d823` |
| ReceiverCondition | `0x321651df4593DA57C413579c5b611D1A90168a3A` |
| AlwaysTrueCondition | `0x2ef2A6162aEF9Df1022ff51c011af94D99AB4904` |
All addresses are available programmatically via `@x402r/core`'s `getChainConfig(chainId)`. See [SDK Overview](/sdk/overview) for details.
## Next Steps
AuthCaptureEscrow and ERC3009PaymentCollector.
Refund request lifecycle and approvals.
The core operator contract.
# ReceiverRefundCollector
Source: https://docs.x402r.org/contracts/periphery/receiver-refund-collector
Pulls funds from receiver for refunds
## Overview
* **Type:** Singleton (one per network)
* **Purpose:** Collect tokens from the receiver to refund the payer after capture
* **Address:** `0x88C9826dFA17Ad9d3a726015C667dD995394D341` (all chains)
## Features
* **Refunds (after capture)** - Pulls funds from the receiver's wallet after the escrow has already released them
* **Receiver approval required** - The receiver must approve the collector contract or supply a signature
* **Operator integration** - `operator.refund()` invokes the collector through the token collector interface
## How it works
Once the escrow has released funds to the receiver (state: `Captured`), refunding the payer requires pulling tokens back out of the receiver's wallet. The `ReceiverRefundCollector` handles that flow:
1. Operator calls `refund(paymentInfo, amount, receiverRefundCollector, collectorData)`
2. The collector transfers tokens from the receiver to the escrow contract
3. The escrow contract returns tokens to the payer
The receiver must pre-approve the `ReceiverRefundCollector` for token transfers, or `collectorData` must carry a valid receiver signature authorizing the refund.
# RefundRequestEvidence
Source: https://docs.x402r.org/contracts/periphery/refund-request-evidence
On-chain evidence submission for refund disputes
## Overview
* **Type:** Singleton (one per network)
* **Purpose:** Store evidence for refund disputes on-chain
* **Address:** `0x4089A5A853e9eF35f504B842795fB272dF69c739` (all chains)
## Features
* **IPFS CID storage** - Stores content hashes on-chain for evidence trails
* **EIP-712 signature approval** - Arbiter approves refunds with off-chain signatures (gas-free for arbiters)
* **Evidence indexing** - The contract indexes evidence by payment and submitting party
* **Multi-party submission** - Both payer and receiver can submit evidence
## How it works
When a payer or receiver disputes a refund, each side submits evidence (documents, screenshots, logs) to IPFS and records the CID on-chain. The arbiter reviews evidence off-chain and produces an EIP-712 approval signature that anyone can relay.
This keeps dispute resolution costs low, since the arbiter never has to submit an on-chain transaction.
# What is x402r?
Source: https://docs.x402r.org/index
x402r adds escrow deposits, refund windows, and dispute resolution to HTTP-native payments
**x402r** is a refundable payments protocol extension for [x402](https://www.x402.org/). It enables secure, reversible transactions with built-in buyer protection through smart contract escrow on Base.
## Why x402r
Standard x402 payments are immediate and irreversible. x402r adds:
* **Escrow deposits**: smart contracts hold funds until conditions clear
* **Refund windows**: configurable time periods for buyers to request refunds
* **Dispute resolution**: arbiter system for handling contested transactions
## How it works
```mermaid theme={null}
sequenceDiagram
participant Client
participant Merchant
participant Escrow
participant Arbiter
Client->>Escrow: Pay (funds held)
Escrow-->>Merchant: Payment notification
alt Happy path
Merchant->>Escrow: Capture funds
Escrow->>Merchant: Transfer
else Refund requested
Client->>Escrow: Request refund
Merchant->>Escrow: Approve/Deny
alt Disputed
Arbiter->>Escrow: Resolve dispute
end
end
```
## Who this is for
Capture funds, process refunds, and manage escrow periods.
## Get started
Understand how x402r extends the x402 protocol.
Explore the escrow and payment operator contracts.
Start building with the TypeScript SDK.
Deploy your own PaymentOperator on Base.
## Architecture
x402r consists of these core components:
| Component | Purpose |
| ------------------------- | ------------------------------------------------------------------------------------------- |
| **PaymentOperator** | Manages payment authorization, capture, charge, void, and refunds with pluggable conditions |
| **AuthCaptureEscrow** | Holds ERC-20 tokens during the payment lifecycle (from commerce-payments) |
| **Conditions & Hooks** | Pluggable authorization checks (before action) and state updates (after action) |
| **EscrowPeriod & Freeze** | Time-based capture and freeze policies for buyer protection |
| **RefundRequest** | Handles refund request lifecycle and approvals |
All protocol contracts use universal CREATE2 addresses, same address on every supported chain.
## Supported networks
Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. More EVMs land as canonical `base/commerce-payments@v1.0.0` coverage extends. See [Network support](/sdk/overview#network-support) for chain IDs and USDC token addresses.
## Resources
Source code and examples.
SDK documentation and API reference.
Get help with integration.
# CLI
Source: https://docs.x402r.org/sdk/cli
One-shot command-line tool for paying x402 endpoints. Wallet-agnostic with zero provider dependencies.
`@x402r/cli` makes a single x402 payment from the command line. You point it at an address, provide a signer, and get back the response body plus a settlement transaction hash.
The CLI carries zero provider SDK dependencies. Raw private keys, JSON-RPC signers (Privy, Turnkey, Fireblocks, Safe), and custom signer modules all work through the same interface.
### Install
```bash npm theme={null}
npx @x402r/cli pay [options]
```
```bash pnpm theme={null}
pnpm dlx @x402r/cli pay [options]
```
```bash bun theme={null}
bunx @x402r/cli pay [options]
```
No project install required. Pin the version (for example, `@x402r/cli@0.2.0`) for reproducible scripted workflows.
### Usage
```bash theme={null}
x402r pay [signer flags] [--chain ] [--rpc ] [--max-amount N] [--json]
```
If the endpoint does not return HTTP 402, the CLI short-circuits and prints the response body with exit code 0. The CLI sends no payment.
### Signer configuration
Configure exactly one signer source. CLI flags take precedence over environment variables. If the CLI finds zero or more than one source, it exits with code 6.
| Source | Flag | Environment variable |
| --------------- | ------------------------------------------------- | --------------------------------- |
| Raw private key | `--key 0x...` | `PRIVATE_KEY` |
| Remote JSON-RPC | `--signer-url ` and `--signer-address 0x...` | `SIGNER_URL` and `SIGNER_ADDRESS` |
| Custom module | `--signer-module ` | `SIGNER_MODULE` |
Environment variable names use no `X402R_` prefix to match Foundry, Hardhat, and x402-reference conventions.
### Request options
| Flag | Description |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--chain ` | Select a specific `accepts[]` entry when the merchant offers more than one chain. Required when more than one option exists. |
| `--asset-transfer-method ` | Select the token-collection path when the chosen `accepts[]` entry supports both EIP-3009 and Permit2. Required when more than one option exists. |
| `--rpc ` | Override the RPC endpoint for on-chain reads. Required for chain IDs not in `viem/chains`. |
| `--max-amount ` | Refuse to pay more than `n` atomic token units. Exits with code 3 if the price exceeds this. |
| `--json` | Emit a single JSON envelope to stdout instead of plain text. |
### Supported chains
The CLI reads the chain from the 402 response's `accepts[].network` field. Any EVM chain known to `viem/chains` works, including Base and Base Sepolia. For chain IDs `viem/chains` does not recognize, pass `--rpc ` with an RPC endpoint.
### Exit codes
| Code | Meaning |
| ---- | --------------------------------------------------------------------------- |
| 0 | Success |
| 1 | Network error |
| 2 | Malformed 402 response or unusable `accepts[]` |
| 3 | Price exceeds `--max-amount` |
| 4 | Signature rejected |
| 5 | Settlement failed (merchant error after payment, or facilitator error) |
| 6 | Signer resolution failed (none, more than one, or incomplete configuration) |
### Examples
#### Raw private key
```bash theme={null}
PRIVATE_KEY=0xabc123... npx @x402r/cli pay https://api.example.com/paid-endpoint
```
#### JSON-RPC signer
Any endpoint that speaks `eth_signTypedData_v4` works: Privy wallet RPC, Turnkey, Fireblocks, Safe, a local `cast wallet` endpoint, or a hardware wallet behind an RPC bridge.
```bash theme={null}
npx @x402r/cli pay https://api.example.com/paid-endpoint \
--signer-url https://signer.example/rpc \
--signer-address 0x586486394C38A2a7d36B16a3FDaF366cd202d823
```
#### Custom module (Privy)
```javascript privy-signer.js theme={null}
import { PrivyClient } from "@privy-io/server-auth";
import { createViemAccount } from "@privy-io/server-auth/viem";
export default async function () {
const privy = new PrivyClient(
process.env.PRIVY_APP_ID,
process.env.PRIVY_APP_SECRET
);
return createViemAccount({
walletId: process.env.PRIVY_WALLET_ID,
address: process.env.PRIVY_WALLET_ADDRESS,
privy,
});
}
```
```bash theme={null}
npx @x402r/cli pay https://api.example.com/paid-endpoint \
--signer-module ./privy-signer.js
```
#### Custom module (Coinbase CDP)
```javascript cdp-signer.js theme={null}
import { CdpClient } from "@coinbase/cdp-sdk";
import { toAccount } from "viem/accounts";
export default async function () {
const cdp = new CdpClient();
const acct = await cdp.evm.getOrCreateAccount({
name: process.env.CDP_ACCOUNT_NAME,
});
return toAccount(acct);
}
```
```bash theme={null}
npx @x402r/cli pay https://api.example.com/paid-endpoint \
--signer-module ./cdp-signer.js
```
### JSON output
With `--json`, the CLI writes a single JSON envelope to stdout:
```json theme={null}
{
"body": "",
"status": 200,
"tx": "0x...",
"elapsedMs": 1234,
"signer": { "kind": "key", "address": "0x..." }
}
```
The CLI drops the `signer` field when the endpoint returned a non-402 response (the CLI sent no payment).
### Signer module contract
A custom signer module must default-export a factory function with the signature `() => Promise`. The returned object must be a viem `Account` with at least `address` and `signTypedData`. The CLI only needs typed-data signatures; the facilitator broadcasts the transaction.
### Programmatic usage
You can also use the `pay` function and `resolveSigner` directly from TypeScript:
```typescript theme={null}
import { pay } from "@x402r/cli";
import type { PayResult } from "@x402r/cli";
const result: PayResult = await pay({
url: "https://api.example.com/paid-endpoint",
key: process.env.PRIVATE_KEY,
json: true,
});
console.log(result.body);
console.log(result.tx);
```
The programmatic API uses the same `PayFlags` interface as the CLI binary. All options (chain, rpc, maxAmount, signer flags) are available.
### Exports
The `@x402r/cli` package exports:
| Export | Type | Description |
| ------------------------ | -------- | ----------------------------------------------------- |
| `pay` | function | Execute a one-shot payment against an endpoint |
| `resolveSigner` | function | Resolve a signer from flags and environment variables |
| `CliError` | class | Base error class with typed exit codes |
| `NetworkError` | class | Exit code 1 |
| `Malformed402Error` | class | Exit code 2 |
| `MaxAmountExceededError` | class | Exit code 3 |
| `SignatureRejectedError` | class | Exit code 4 |
| `SettlementError` | class | Exit code 5 |
| `SignerResolutionError` | class | Exit code 6 |
## Next steps
Accept payments and manage escrow releases.
Runnable examples for every SDK operation.
# Create x402r Client
Source: https://docs.x402r.org/sdk/create-client
Client factory, role presets, and configuration reference.
### Full Client
`createX402r()` returns a client with all action groups. No type restrictions.
```typescript theme={null}
import { createX402r } from '@x402r/sdk'
const client = createX402r({
publicClient,
walletClient, // optional for read-only
operatorAddress: '0x...', // from deploy result
escrowPeriodAddress: '0x...', // from deploy result
refundRequestAddress: '0x...', // from deploy result
refundRequestEvidenceAddress: '0x...', // from deploy result
freezeAddress: '0x...', // from deploy result
})
await client.payment.getAmounts(paymentInfo)
await client.refund?.request(paymentInfo, amount)
await client.escrow?.isDuringEscrow(paymentInfo)
```
### Role Presets
Role presets call `createX402r()` internally and narrow the TypeScript types so autocomplete only shows relevant methods. All three require `walletClient`.
```typescript theme={null}
import {
createPayerClient,
createMerchantClient,
createArbiterClient,
} from '@x402r/sdk'
const payer = createPayerClient({ publicClient, walletClient, operatorAddress: '0x...' })
const merchant = createMerchantClient({ publicClient, walletClient, operatorAddress: '0x...' })
const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...' })
```
Type narrowing is a DX convenience, not a security boundary. On-chain [conditions](/contracts/conditions/overview) enforce access control.
### Config Reference
| Field | Type | Required | Notes |
| --------------------------------- | -------------- | :------: | ----------------------------------------------------------------------------- |
| `publicClient` | `PublicClient` | Yes | viem public client for reads |
| `walletClient` | `WalletClient` | No | Required for writes. Role presets throw without it. |
| `operatorAddress` | `Address` | Yes | Your deployed PaymentOperator |
| `chainId` | `number` | No | Resolves from `publicClient.chain` when omitted |
| `network` | `string` | No | EIP-155 network ID (for example, `'eip155:84532'`). Alternative to `chainId`. |
| `escrowPeriodAddress` | `Address` | No | Activates `escrow` group |
| `refundRequestAddress` | `Address` | No | Activates `refund` group |
| `refundRequestEvidenceAddress` | `Address` | No | Activates `evidence` group (requires `refundRequestAddress`) |
| `freezeAddress` | `Address` | No | Activates `freeze` group |
| `paymentIndexRecorderHookAddress` | `Address` | No | Activates `query` group |
| `paymentStore` | `PaymentStore` | No | Custom storage layer for payment lookups |
| `eventFromBlock` | `bigint` | No | Starting block for event-based payment lookups |
### Action Groups
| Group | Methods | Required config |
| ---------- | ------- | --------------------------------- |
| `payment` | 9 | Always available |
| `operator` | 8 | Always available |
| `watch` | 4 | Always available |
| `escrow` | 3 | `escrowPeriodAddress` |
| `refund` | 14 | `refundRequestAddress` |
| `evidence` | 4 | `refundRequestEvidenceAddress` |
| `freeze` | 3 | `freezeAddress` |
| `query` | 3 | `paymentIndexRecorderHookAddress` |
Groups without their required address are `undefined` on the client. Use optional chaining:
```typescript theme={null}
await client.escrow?.isDuringEscrow(paymentInfo) // undefined if no escrowPeriodAddress
```
### Extend
Add custom action groups with `.extend()`:
```typescript theme={null}
import { createX402r, queryActions } from '@x402r/sdk'
const client = createX402r({ publicClient, operatorAddress: '0x...' })
const extended = client.extend(
queryActions('0xHookAddress', { eventFromBlock: 100000n })
)
// extended.query is now defined
const payments = await extended.query.getPayerPayments(payerAddress)
```
### ERC-8004 plugin
The `erc8004Actions` plugin adds `identity`, `reputation`, and `discovery` action groups for on-chain agent identity and reputation:
```typescript theme={null}
import { createX402r, erc8004Actions } from '@x402r/sdk'
const client = createX402r({ publicClient, walletClient, operatorAddress: '0x...' })
const extended = client.extend(erc8004Actions())
// Identity
await extended.identity.register('https://my-agent.example.com')
await extended.identity.verifyAgentId(42n, '0xAgentAddress...')
await extended.identity.resolveAgent(42n)
await extended.identity.isRegistered('0xAgentAddress...')
// Verify + reputation in one call
const result = await extended.identity.check(42n, '0xAgentAddress...', [
'0xReviewer1...',
'0xReviewer2...',
])
console.log('Verified:', result.verified)
console.log('Reputation:', result.reputation) // ReputationSummary | null
// Reputation
await extended.reputation.rate(42n, 85)
await extended.reputation.getSummary(42n, ['0xReviewer...'])
// Discovery
await extended.discovery.resolveServiceEndpoint(42n, 'arbiter')
```
#### `identity.check()`
Verifies an agent's on-chain identity and optionally fetches their reputation summary in a single call. Both the verification and reputation lookup run in parallel.
```typescript theme={null}
const { verified, reputation } = await extended.identity.check(
agentId,
agentAddress,
reviewerAddresses, // optional
)
```
| Parameter | Type | Description |
| ----------- | -------------------- | --------------------------------------------------------- |
| `agentId` | `bigint` | The agent's on-chain ID |
| `address` | `Address` | The address claiming to own the agent ID |
| `reviewers` | `readonly Address[]` | Optional list of reviewer addresses for reputation lookup |
Returns `CheckAgentResult`:
```typescript theme={null}
interface CheckAgentResult {
verified: boolean
reputation: ReputationSummary | null
}
```
If you omit `reviewers` or pass an empty array, `reputation` is `null` and only on-chain verification runs.
For standalone helpers that extract identity data from x402 extension responses without a client instance, use the `extractArbiterIdentity`, `extractReputationRegistrations`, and `fetchArbiterIdentity` exports from `@x402r/sdk`.
## Next steps
Accept payments and capture funds.
Get the addresses for your client config.
# Arbiter Setup
Source: https://docs.x402r.org/sdk/delivery-arbiter
Build a service that evaluates responses and releases funds.
### Prerequisites
* A wallet with ETH on Base Sepolia for gas ([faucet](https://www.alchemy.com/faucets/base-sepolia))
* Node.js 18+ and npm
* Operator and escrow addresses from the [Merchant Setup](/sdk/delivery-merchant)
The [AI garbage detector example](https://github.com/BackTrackCo/arbiter-examples) implements this pattern end-to-end with heuristic plus LLM evaluation.
```bash npm theme={null}
npm install @x402r/sdk @x402r/helpers
```
```bash pnpm theme={null}
pnpm add @x402r/sdk @x402r/helpers
```
```bash bun theme={null}
bun add @x402r/sdk @x402r/helpers
```
The role-narrowed `createArbiterClient` exposes `payment.voidPayment`, `payment.getState`, and `payment.getAmounts`. Capturing requires the full surface, so use `createX402r()` directly:
```typescript theme={null}
import { createPublicClient, createWalletClient, http } from 'viem'
import { baseSepolia } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import { createX402r } from '@x402r/sdk'
const account = privateKeyToAccount(process.env.ARBITER_PRIVATE_KEY as `0x${string}`)
const arbiter = createX402r({
publicClient: createPublicClient({ chain: baseSepolia, transport: http() }),
walletClient: createWalletClient({
account,
chain: baseSepolia,
transport: http(),
}),
operatorAddress: process.env.OPERATOR_ADDRESS as `0x${string}`,
escrowPeriodAddress: process.env.ESCROW_PERIOD_ADDRESS as `0x${string}`,
})
```
The merchant's `forwardToArbiter()` hook POSTs `{ responseBody, transaction, paymentInfoWire }` to `/verify`. The `paymentInfoWire` is the JSON-safe wire form of `PaymentInfo`; call `PaymentInfo.fromWire(...)` to recover the `bigint`-typed struct expected by SDK actions.
```typescript theme={null}
import { PaymentInfo } from '@x402r/sdk'
import express from 'express'
const app = express()
app.use(express.json())
app.post('/verify', async (req, res) => {
const { responseBody, transaction, paymentInfoWire } = req.body
if (!paymentInfoWire) {
res.status(400).json({ error: 'missing_payment_info' })
return
}
const paymentInfo = PaymentInfo.fromWire(paymentInfoWire)
const passed = await evaluate(responseBody)
if (passed) {
const amounts = await arbiter.payment.getAmounts(paymentInfo)
await arbiter.payment.capture(paymentInfo, amounts.capturableAmount)
res.json({ verdict: 'PASS' })
} else {
// Arbiter can refund immediately without waiting for escrow expiry.
await arbiter.payment.voidPayment(paymentInfo)
res.json({ verdict: 'FAIL' })
}
})
app.listen(3001)
```
The `evaluate()` function is where your logic lives. It can run:
* **Heuristic checks**: HTTP status code, response size, content-type validation
* **AI judgment**: send response body to an LLM and ask "is this a valid response?"
* **Schema validation**: check if the response matches an expected JSON schema
```typescript theme={null}
async function evaluate(responseBody: string): Promise {
// Reject empty or error responses
if (!responseBody || responseBody.length < 10) return false
if (responseBody.includes('"error"')) return false
// LLM evaluation
// const result = await llm.evaluate(responseBody)
// return result.verdict === 'PASS'
return true
}
```
With delivery protection, the arbiter can call `voidPayment()` immediately on a FAIL verdict. You do not need to wait for escrow expiry. The receiver (merchant) can also trigger a voluntary refund at any time.
If your service goes down, no payments get evaluated and funds stay in escrow until timeout. The escrow period protects payers, but add uptime monitoring and alerting.
## Next Steps
Deploy the operator and configure forwardToArbiter().
Runnable examples for every SDK operation.
# Merchant Setup
Source: https://docs.x402r.org/sdk/delivery-merchant
Configure forwardToArbiter() to send responses to the arbiter for evaluation.
### Prerequisites
* A deployed delivery protection operator (see [Deploy an Operator](/sdk/deploy-operator#delivery-protection-operator))
* An arbiter service endpoint (see [Arbiter Setup](/sdk/delivery-arbiter))
```bash npm theme={null}
npm install @x402r/helpers
```
```bash pnpm theme={null}
pnpm add @x402r/helpers
```
```bash bun theme={null}
bun add @x402r/helpers
```
Add the `forwardToArbiter()` hook to your x402 resource server. After every successful `auth-capture` settlement, it POSTs to your arbiter service fire-and-forget:
```typescript theme={null}
import { forwardToArbiter } from '@x402r/helpers'
import { AuthCaptureEvmScheme } from '@x402r/evm/auth-capture/server'
const resourceServer = new x402ResourceServer(facilitatorConfig)
.register(networkId, new AuthCaptureEvmScheme())
.onAfterSettle(
forwardToArbiter('http://your-arbiter:3001', {
onError: (err) => console.error('Arbiter unreachable:', err),
}),
)
```
The hook POSTs to `{arbiterUrl}/verify` with:
```json theme={null}
{
"responseBody": "the HTTP response body as a string",
"transaction": "0xsettlement_tx_hash",
"paymentInfoWire": {
"operator": "0x...",
"payer": "0x...",
"receiver": "0x...",
"token": "0x...",
"maxAmount": "10000",
"preApprovalExpiry": 1740758554,
"authorizationExpiry": 1740762154,
"refundExpiry": 1741276954,
"minFeeBps": 0,
"maxFeeBps": 500,
"feeReceiver": "0x...",
"salt": "0x..."
}
}
```
The helper reconstructs `PaymentInfoWire` from the verified `SettleResultContext`. The arbiter consumes `req.body.paymentInfoWire` and runs it through `PaymentInfo.fromWire(...)` to recover the `bigint`-typed struct expected by SDK actions. See [forwardToArbiter() docs](/sdk/helpers/forward-to-arbiter) for the full payload shape.
`forwardToArbiter()` is fire-and-forget. If the arbiter service is unreachable, funds stay in escrow until timeout. Add monitoring for arbiter availability.
The arbiter service needs `operatorAddress` and `escrowPeriodAddress` from your [deployment](/sdk/deploy-operator#delivery-protection-operator) to construct its SDK client. Share these via config, environment variables, or a shared registry.
## Next Steps
Build the service that evaluates responses and releases funds.
Full deployment config and condition slot details.
# Delivery Protection
Source: https://docs.x402r.org/sdk/delivery-protection
Automated quality verification for every transaction.
In the delivery protection model, the arbiter evaluates every transaction. The arbiter or a satisfied payer can capture funds. If the arbiter issues a FAIL verdict, it can trigger an immediate refund without waiting for escrow expiry. If nobody acts, funds return to the payer once escrow expires.
This differs from the [marketplace model](/sdk/overview) where the merchant releases funds and the arbiter only gets involved when a payer files a dispute.
| | Marketplace | Delivery Protection |
| -------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- |
| Who releases funds | Merchant (after escrow) | Arbiter or payer |
| Refund during escrow | Receiver or arbiter | Escrow expiry, receiver, or arbiter |
| Dispute process | Payer files refund request | No disputes needed |
| Arbiter involvement | Only on disputes | Every transaction |
| Contracts deployed | \~8 (Operator, EscrowPeriod, RefundRequest, Evidence, Freeze, etc.) | 6 (Operator, EscrowPeriod, SAC, 2x OrCondition, HookCombinator) |
| Deploy preset | `deployMarketplaceOperator()` | `deployDeliveryProtectionOperator()` |
Use this when every response needs programmatic quality checks: AI content verification, garbage detection, schema validation.
Deploy the operator and configure forwardToArbiter().
Build the service that evaluates responses and releases funds.
# Deploy an operator
Source: https://docs.x402r.org/sdk/deploy-operator
Deploy a PaymentOperator with escrow, freeze, and dispute resolution in one call
The `@x402r/core` package includes deployment presets that handle the full lifecycle of deploying a PaymentOperator and all its supporting contracts.
## Presets
The SDK ships two deployment presets. Pick the one that matches your use case:
| Preset | Use case | Freeze | Fees | RefundRequest |
| ---------------------------------- | ------------------------------------------- | -------------- | -------------- | ------------- |
| `deployMarketplaceOperator` | General marketplace with dispute resolution | Yes (optional) | Yes (optional) | Yes |
| `deployDeliveryProtectionOperator` | Garbage detection / delivery verification | No | No | No |
All contracts ship via CREATE2 factories, so identical configurations produce identical addresses across deployments.
## Marketplace operator
A complete marketplace operator deployment includes:
1. **EscrowPeriod**: Records authorization time, enforces waiting period before capture
2. **Freeze**: Allows payer to freeze payment during escrow, receiver to unfreeze
3. **ReceiverCondition**: Gates voids to the merchant (receiver)
4. **RefundRequest (`IHook`)**: Wired as `voidPostActionHook`, flips pending refund requests to `Approved` during `voidPayment()`
5. **StaticFeeCalculator**: Optional operator fee (basis points)
6. **PaymentOperator**: The main contract tying everything together
## Deploy your operator
**Prerequisites:**
* Node.js 20+, pnpm 9.15+
* A private key with Base Sepolia ETH ([get Sepolia ETH](https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet))
Call `deployMarketplaceOperator` from `@x402r/core` with a viem wallet client. Because every contract uses CREATE2, deploys are idempotent: re-running with the same parameters reuses any existing contract at the predicted address and skips it.
```typescript theme={null}
import { createPublicClient, createWalletClient, http } from 'viem';
import { baseSepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import { deployMarketplaceOperator } from '@x402r/core'
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
})
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(),
})
const result = await deployMarketplaceOperator(
walletClient,
publicClient,
{
chainId: 84532, // Base Sepolia
feeReceiver: account.address, // receives operator fees
arbiter: '0xArbiterAddress...', // dispute resolver
escrowPeriodSeconds: 604800n, // 7 days
freezeDurationSeconds: 259200n, // 3 days max freeze
operatorFeeBps: 100n, // 1% fee (optional)
}
);
console.log('Operator:', result.operatorAddress);
console.log('EscrowPeriod:', result.escrowPeriodAddress);
console.log('Freeze:', result.freezeAddress);
console.log('New deployments:', result.summary.newCount);
console.log('Existing (reused):', result.summary.existingCount);
```
## Configuration options
| Option | Type | Description |
| ----------------------- | --------- | --------------------------------------------------------------------------------------------------- |
| `chainId` | `number` | Target chain ID (for example, `84532` for Base Sepolia) |
| `feeReceiver` | `Address` | Address that receives operator fees |
| `arbiter` | `Address` | Arbiter address for dispute resolution |
| `escrowPeriodSeconds` | `bigint` | Escrow waiting period (for example, `604800n` for 7 days) |
| `freezeDurationSeconds` | `bigint` | How long freezes last. Default: `0n` (permanent until unfrozen) |
| `operatorFeeBps` | `bigint` | Fee in basis points. Default: `0n` (no fee). `100n` = 1% |
| `authorizedCodehash` | `Hex` | Optional. Restricts which contract codehashes can record. Defaults to `bytes32(0)` (no restriction) |
## Deployment result
```typescript theme={null}
interface MarketplaceOperatorDeployment {
operatorAddress: Address // The PaymentOperator
escrowPeriodAddress: Address // EscrowPeriod hook/condition
freezeAddress: Address | null // Freeze condition (null if disabled)
refundRequestAddress: Address // RefundRequest contract
refundRequestEvidenceAddress: Address // RefundRequestEvidence contract
voidConditionAddress: Address // OR(Receiver, Arbiter)
feeCalculatorAddress: Address | null // null if no fee
operatorConfig: OperatorConfig // Full operator slot configuration
deployments: DeployResult[] // Per-contract deploy details
summary: {
newCount: number // Newly deployed contracts
existingCount: number // Reused existing contracts
txHashes: `0x${string}`[] // All deployment tx hashes
}
}
```
Because all contracts use CREATE2, redeploying with the same parameters is idempotent. The tooling skips any contract that already exists at the predicted address. The `summary` tells you what was new vs reused.
## Preview addresses (no deploy)
```typescript theme={null}
import { previewMarketplaceOperator } from '@x402r/core'
const preview = await previewMarketplaceOperator(publicClient, {
chainId: 84532,
feeReceiver: account.address,
arbiter: '0xArbiterAddress...',
escrowPeriodSeconds: 604800n,
})
console.log('Operator will be at:', preview.operatorAddress)
console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress)
```
## Marketplace operator slot configuration
The deployed marketplace operator has the following slot configuration:
| Slot | Contract | Purpose |
| -------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------- |
| `AUTHORIZE_PRE_ACTION_CONDITION` | (none) | Default: anyone with a valid signature can authorize |
| `AUTHORIZE_POST_ACTION_HOOK` | EscrowPeriod | Records authorization timestamp |
| `CHARGE_PRE_ACTION_CONDITION` | (none) | No restrictions on charge |
| `CAPTURE_PRE_ACTION_CONDITION` | EscrowPeriod (or AND(EscrowPeriod, Freeze) if freeze enabled) | Blocks capture during escrow period |
| `VOID_PRE_ACTION_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve |
| `VOID_POST_ACTION_HOOK` | RefundRequest | Tracks refund request state |
| `REFUND_PRE_ACTION_CONDITION` | Receiver | Only receiver after escrow |
| `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee (if configured) |
| `FEE_RECEIVER` | Your address | Receives fees |
***
## Network support
The deploy presets target the chains in `@x402r/core`'s `x402rChains` (Base and Base Sepolia today). See [Network support](/sdk/overview#network-support) for chain IDs, EIP-155 IDs, and token addresses.
Deployment requires gas fees. Ensure your wallet has ETH on the target network. On Base Sepolia, you can fund a wallet from [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets).
## Delivery Protection Operator
For programmatic quality verification (AI garbage detection, schema validation), use the delivery protection preset. No RefundRequest, Evidence, or Freeze contracts. The arbiter or payer can capture funds, and the arbiter can issue immediate refunds without waiting for escrow expiry.
```typescript theme={null}
import { createPublicClient, createWalletClient, http } from 'viem';
import { baseSepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import { deployDeliveryProtectionOperator } from '@x402r/core'
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
})
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(),
})
const deployment = await deployDeliveryProtectionOperator(
walletClient,
publicClient,
{
chainId: 84532,
arbiter: '0xArbiterServiceAddress',
feeReceiver: account.address,
escrowPeriodSeconds: 300n, // 5 minutes
},
)
console.log('Operator:', deployment.operatorAddress)
console.log('EscrowPeriod:', deployment.escrowPeriodAddress)
console.log('ArbiterCondition:', deployment.arbiterConditionAddress)
console.log('ReleaseCondition:', deployment.captureConditionAddress)
console.log('AuthorizeHook:', deployment.authorizeHookAddress)
```
| Option | Type | Description |
| --------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------- |
| `chainId` | `number` | Target chain |
| `arbiter` | `Address` | Arbiter address for capture and refund decisions |
| `feeReceiver` | `Address` | Receives protocol fees |
| `escrowPeriodSeconds` | `bigint` | Verification window before automatic refund |
| `authorizedCodehash` | `Hex` | Override the default `hookCombinatorCodehash`. Optional |
| `paymentIndexRecorderHookAddress` | `Address` | Override the default PaymentIndexRecorderHook. Pass `zeroAddress` to skip on-chain payment indexing. Optional |
| `allowArbiterRefund` | `boolean` | Lets the arbiter refund immediately during escrow. Default: `false` |
```typescript theme={null}
interface DeliveryProtectionOperatorDeployment {
operatorAddress: Address
escrowPeriodAddress: Address
arbiterConditionAddress: Address
captureConditionAddress: Address // OrCondition([arbiter, payer])
voidConditionAddress: Address // OrCondition([escrowPeriod, receiver, arbiter])
authorizeHookAddress: Address // HookCombinator([escrowPeriod, paymentIndexRecorderHook])
paymentIndexRecorderHookAddress: Address
operatorConfig: OperatorConfig
deployments: DeployResult[]
summary: {
newCount: number
existingCount: number
txHashes: `0x${string}`[]
}
}
```
Deploys 6 contracts by default: EscrowPeriod, StaticAddressCondition(arbiter), OrCondition(release), OrCondition(refund), HookCombinator, and the Operator. If you pass `paymentIndexRecorderHookAddress: zeroAddress`, the HookCombinator is skipped (5 contracts).
Redeploying with the same parameters is idempotent (CREATE2). The tooling reuses any contract that already exists at the predicted address.
Compute addresses without deploying:
```typescript theme={null}
import { previewDeliveryProtectionOperator } from '@x402r/core'
const preview = await previewDeliveryProtectionOperator(publicClient, {
chainId: 84532,
arbiter: '0xArbiterServiceAddress',
feeReceiver: account.address,
escrowPeriodSeconds: 300n,
})
console.log('Operator will be at:', preview.operatorAddress)
console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress)
console.log('AuthorizeHook will be at:', preview.authorizeHookAddress)
```
| Slot | Contract | Purpose |
| ------------------------------ | ------------------------------------------------------------- | --------------------------------------------------------------------- |
| `CAPTURE_PRE_ACTION_CONDITION` | OrCondition(\[SAC(arbiter), PayerCondition]) | Arbiter or satisfied payer can capture |
| `AUTHORIZE_POST_ACTION_HOOK` | HookCombinator(\[EscrowPeriod, PaymentIndexRecorderHook]) | Records authorization time and indexes payments on-chain |
| `VOID_PRE_ACTION_CONDITION` | OrCondition(\[EscrowPeriod, ReceiverCondition, SAC(arbiter)]) | Escrow expiry, receiver voluntary refund, or arbiter immediate refund |
| `REFUND_PRE_ACTION_CONDITION` | ReceiverCondition | Only receiver after escrow |
## Next Steps
Accept payments, capture funds from escrow.
See working merchant and client examples.
Forward escrow settlements to an arbiter service.
On-chain architecture, conditions, and hooks.
# Examples
Source: https://docs.x402r.org/sdk/examples
Runnable examples for every SDK operation.
The [x402r-sdk repository](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples) ships runnable example scripts organized by role plus end-to-end scenarios.
## Examples
Request a refund, freeze a payment, submit on-chain evidence (a placeholder CID; the integrator owns IPFS pinning). Three TypeScript scripts.
Capture from escrow and charge directly. TypeScript scripts plus README.
Approve a refund, review on-chain evidence, distribute protocol fees.
End-to-end runners: happy-path capture, dispute resolution, atomic charge, partial refund flow. Wires payer + merchant + arbiter together against a local Anvil fork.
Shared setup utilities: Anvil-fork bootstrap, constants, common types.
## Running examples
All examples run against a local Anvil fork seeded by `shared/anvil-setup.ts`. You do not need a mainnet wallet or funding.
```bash pnpm theme={null}
git clone https://github.com/BackTrackCo/x402r-sdk.git
cd x402r-sdk
pnpm install && pnpm build
```
```bash bun theme={null}
git clone https://github.com/BackTrackCo/x402r-sdk.git
cd x402r-sdk
bun install && bun run build
```
The SDK uses pnpm workspaces (`pnpm@10.23.0`). The `npm` runtime is fine for application code that consumes published `@x402r/*` packages, but the workspace clone above expects pnpm or a workspace-aware install.
See each example directory's README on GitHub for the exact run command for that script.
## Next steps
Walk through `deployMarketplaceOperator()` and `deployDeliveryProtectionOperator()`.
Forward `auth-capture` settlements to an arbiter service.
Browse every example.
# forwardToArbiter()
Source: https://docs.x402r.org/sdk/helpers/forward-to-arbiter
Forward settlement data to an arbiter service for quality evaluation
The `forwardToArbiter()` function creates an `onAfterSettle` hook that forwards the response body and reconstructed `PaymentInfoWire` to an arbiter service. It runs fire-and-forget so it never blocks the response to the client.
* Only fires for successful **`auth-capture`** scheme settlements
* POSTs `{ responseBody, transaction, paymentInfoWire }` to `{arbiterUrl}/verify`
* The hook catches errors internally so an unreachable arbiter cannot break the payment flow
## Usage
```typescript theme={null}
import { forwardToArbiter } from '@x402r/helpers'
import { AuthCaptureEvmScheme } from '@x402r/evm/auth-capture/server'
const resourceServer = new x402ResourceServer(facilitatorClient)
.register(networkId, new AuthCaptureEvmScheme())
.onAfterSettle(
forwardToArbiter('http://arbiter:3001'),
)
```
## Function signature
```typescript theme={null}
function forwardToArbiter(
arbiterUrl: string,
options?: ForwardToArbiterOptions,
): (context: SettleResultContext) => Promise
```
### Parameters
| Parameter | Type | Description |
| ------------ | ------------------------- | -------------------------------------------------------------------------- |
| `arbiterUrl` | `string` | Base endpoint of your arbiter service (for example, `http://arbiter:3001`) |
| `options` | `ForwardToArbiterOptions` | Optional configuration (see below) |
### Options
```typescript theme={null}
interface ForwardToArbiterOptions {
/** Custom error handler. Defaults to `console.warn`. */
onError?: (error: unknown) => void
}
```
## Payload shape
When an `auth-capture` settlement succeeds, the hook POSTs the following JSON to `{arbiterUrl}/verify`:
```typescript theme={null}
{
responseBody: string // UTF-8 encoded response body
transaction: string // Settlement transaction hash
paymentInfoWire: {
operator: `0x${string}` // from extra.captureAuthorizer
payer: `0x${string}` // recovered at settlement
receiver: `0x${string}` // from requirements.payTo
token: `0x${string}` // from requirements.asset
maxAmount: string // from requirements.amount
preApprovalExpiry: number // authorization.validBefore (EIP-3009) or permit2Authorization.deadline (Permit2)
authorizationExpiry: number // from extra.captureDeadline
refundExpiry: number // from extra.refundDeadline
minFeeBps: number // from extra.minFeeBps
maxFeeBps: number // from extra.maxFeeBps
feeReceiver: `0x${string}` // from extra.feeRecipient
salt: string // from payload.salt
}
}
```
The helper reconstructs `PaymentInfoWire` from the verified `SettleResultContext` using the `reconstructPaymentInfoWire()` helper. The arbiter consumes `req.body.paymentInfoWire` and runs it through `PaymentInfo.fromWire(...)` (from `@x402r/sdk` or `@x402r/core`) to get the `bigint`-typed `PaymentInfo` struct expected by SDK actions.
## Error handling
By default, the hook logs fetch errors with `console.warn`. Override this with a custom handler:
```typescript theme={null}
import { forwardToArbiter } from '@x402r/helpers'
import { AuthCaptureEvmScheme } from '@x402r/evm/auth-capture/server'
const resourceServer = new x402ResourceServer(facilitatorClient)
.register(networkId, new AuthCaptureEvmScheme())
.onAfterSettle(
forwardToArbiter('http://arbiter:3001', {
onError: (err) => sentry.captureException(err),
}),
)
```
The hook wraps each error in an `X402rError` carrying the arbiter endpoint and request details for easier debugging.
## Skipped scenarios
The hook returns without making a request when:
* The settlement was not successful (`context.result.success === false`)
* The scheme is not `auth-capture`
* No response body is available in the transport context
## Address re-exports
The `@x402r/helpers` package re-exports chain-invariant address constants from `@x402r/core` for convenience:
```typescript theme={null}
import {
authCaptureEscrow,
tokenCollector,
protocolFeeConfig,
receiverRefundCollector,
factories,
conditions,
getChainConfig,
supportedChainIds,
} from '@x402r/helpers'
```
Plus the `@x402r/evm` wire-format types and guards:
```typescript theme={null}
import {
type AuthCaptureExtra,
type AuthCapturePayload,
type Eip3009Payload,
type Permit2Payload,
type PaymentInfoStruct,
isAuthCaptureExtra,
isAuthCapturePayload,
isEip3009Payload,
isPermit2Payload,
} from '@x402r/helpers'
```
And the `x402rDefaults` builder for hand-constructing `extra` in `PaymentRequirements`:
```typescript theme={null}
import { type X402rDefaultsInput, x402rDefaults } from '@x402r/helpers'
```
`x402rDefaults(input)` returns an `AuthCaptureExtra` populated with sensible defaults, useful when you want to build `PaymentRequirements` outside the merchant client.
## Next steps
See working merchant server examples.
# Merchant Server Quickstart
Source: https://docs.x402r.org/sdk/merchant/getting-started
Accept escrow-backed refundable payments on your Express server in 5 minutes
This guide walks you through setting up an Express server that accepts x402r escrow-backed payments. By the end, you'll have a paid API endpoint protected by the x402 payment middleware with refundable escrow support.
## Prerequisites
* Node.js 20+
* A deployed PaymentOperator contract ([Deploy Operator](/sdk/deploy-operator))
* A running facilitator service
* Base Sepolia ETH for testing
## Setup
```bash theme={null}
mkdir merchant-server && cd merchant-server
npm init -y
npm install express @x402/core @x402/express @x402r/evm dotenv
```
Create a `.env` file in the project root:
```bash theme={null}
# Replace ADDRESS with your merchant address.
ADDRESS=0x321651df4593DA57C413579c5b611D1A90168a3A
# Replace OPERATOR_ADDRESS with the operator you deployed.
OPERATOR_ADDRESS=0xa0d4734842df1690a5B33Cb21828c946e39D55a2
FACILITATOR_URL=http://localhost:4022
```
Create `index.ts`:
```typescript theme={null}
import "dotenv/config";
import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { AuthCaptureEvmScheme } from "@x402r/evm/auth-capture/server";
import { getChainConfig } from "@x402r/core";
import { HTTPFacilitatorClient } from "@x402/core/server";
const address = process.env.ADDRESS as `0x${string}`;
const operatorAddress = process.env.OPERATOR_ADDRESS as `0x${string}`;
if (!address || !operatorAddress) {
console.error("Missing required environment variables: ADDRESS, OPERATOR_ADDRESS");
process.exit(1);
}
const facilitatorUrl = process.env.FACILITATOR_URL;
if (!facilitatorUrl) {
console.error("FACILITATOR_URL environment variable is required");
process.exit(1);
}
const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl });
const networkId = "eip155:84532";
const app = express();
const now = Math.floor(Date.now() / 1000);
app.use(
paymentMiddleware(
{
"GET /weather": {
accepts: [
{
scheme: "auth-capture",
price: "$0.01",
network: networkId,
payTo: address,
maxTimeoutSeconds: 60,
extra: {
name: "USDC",
version: "2",
captureAuthorizer: operatorAddress,
captureDeadline: now + 60 * 60, // capture within 1 hour
refundDeadline: now + 24 * 60 * 60, // refund window 24 hours
feeRecipient: operatorAddress,
minFeeBps: 0,
maxFeeBps: 500,
// assetTransferMethod defaults to "eip3009"
// autoCapture defaults to false (two-phase)
},
},
],
description: "Weather data",
mimeType: "application/json",
},
},
new x402ResourceServer(facilitatorClient).register(
networkId,
new AuthCaptureEvmScheme(),
),
),
);
app.get("/weather", (req, res) => {
res.send({
report: { weather: "sunny", temperature: 70 },
});
});
app.listen(4021, () => {
console.log("Server listening at http://localhost:4021");
});
```
```bash theme={null}
npx tsx index.ts
```
You should see:
```
Server listening at http://localhost:4021
```
```bash theme={null}
curl http://localhost:4021/weather
```
Without a valid payment header, the server responds with HTTP 402 and the auth-capture payment requirements:
```json theme={null}
{
"x402Version": 2,
"accepts": [{
"scheme": "auth-capture",
"price": "$0.01",
"network": "eip155:84532",
"payTo": "0x...",
"maxTimeoutSeconds": 60,
"extra": {
"name": "USDC",
"version": "2",
"captureAuthorizer": "0x...",
"captureDeadline": 1740758554,
"refundDeadline": 1741276954,
"feeRecipient": "0x...",
"minFeeBps": 0,
"maxFeeBps": 500
}
}]
}
```
## How it works
* **`extra` config** declares the captureAuthorizer, capture/refund deadlines, fee recipient, and fee bounds. The canonical `AuthCaptureEscrow` and token collector addresses are universal CREATE2 deploys, so routes do not need to repeat them.
* **`AuthCaptureEvmScheme`** registers the auth-capture payment scheme with the x402 resource server so it can verify auth-capture-backed payments.
* **`paymentMiddleware`** intercepts requests, checks for a valid payment header, and returns 402 when the caller has not provided one.
* **`HTTPFacilitatorClient`** connects to the facilitator service that verifies and settles payments on-chain.
## Next Steps
Forward escrow settlements to an arbiter service.
Capture payments, handle refunds, and manage escrow.
Deploy your own PaymentOperator contract.
# Merchant SDK
Source: https://docs.x402r.org/sdk/merchant/quickstart
Capture funds, charge payments, process refunds, and query escrow state
The `@x402r/sdk` package covers the merchant's post-payment lifecycle: capturing escrowed funds, charging directly, processing refunds, and querying operator state.
**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the `createMerchantClient` factory for managing payments after they arrive.
## Installation
```bash npm theme={null}
npm install @x402r/sdk viem
```
```bash pnpm theme={null}
pnpm add @x402r/sdk viem
```
```bash bun theme={null}
bun add @x402r/sdk viem
```
## Setup
```typescript theme={null}
import { createMerchantClient } from '@x402r/sdk'
import { createPublicClient, createWalletClient, http } from 'viem'
import { baseSepolia } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
const merchant = createMerchantClient({
publicClient: createPublicClient({ chain: baseSepolia, transport: http() }),
walletClient: createWalletClient({
account,
chain: baseSepolia,
transport: http(),
}),
operatorAddress: '0x...',
escrowPeriodAddress: '0x...',
refundRequestAddress: '0x...',
refundRequestEvidenceAddress: '0x...',
freezeAddress: '0x...',
})
```
## payment.capture
Transfer escrowed funds to the receiver. Specify a smaller amount than `paymentInfo.maxAmount` for a partial capture; the rest stays in escrow.
```typescript theme={null}
const tx = await merchant.payment.capture(paymentInfo, 10_000_000n)
```
**Parameters**
| Name | Type | Description |
| ------------- | ------------------ | ------------------------------------------------------------ |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
| `amount` | `bigint` | Atomic units to capture (must be ≤ `paymentInfo.maxAmount`) |
| `data` | `Hex` *(optional)* | Pass-through data for the operator's pre/post action plugins |
**Returns** `Promise`, the settlement transaction hash.
Always query `payment.getAmounts()` first to determine the available capturable amount.
## payment.voidPayment
Return all escrowed funds to the payer before capture. Full-only: `void()` empties the authorization in one transaction. For a partial return, capture the share you want to keep first, then void the rest (or let it expire at `captureDeadline`).
```typescript theme={null}
const tx = await merchant.payment.voidPayment(paymentInfo)
```
**Parameters**
| Name | Type | Description |
| ------------- | ------------------ | ------------------------------------------------------------ |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
| `data` | `Hex` *(optional)* | Pass-through data for the operator's pre/post action plugins |
**Returns** `Promise`, the void transaction hash.
## payment.charge
Non-escrow settlement for subscriptions or session-based payments. Pulls funds directly from the payer via a token collector (no escrow hold).
```typescript theme={null}
const tx = await merchant.payment.charge(
paymentInfo,
5_000_000n,
'0xTokenCollector...' as `0x${string}`,
'0xSignatureData...' as `0x${string}`,
)
```
**Parameters**
| Name | Type | Description |
| ---------------- | ------------- | -------------------------------------------------------------- |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
| `amount` | `bigint` | Atomic units to charge |
| `tokenCollector` | `Address` | Canonical token collector for the chosen `assetTransferMethod` |
| `collectorData` | `Hex` | Raw ERC-3009 signature or ABI-encoded Permit2 signature |
**Returns** `Promise`, the charge transaction hash.
## payment.refund
Refund funds the merchant has already captured. Requires a token collector to pull funds from the merchant's balance.
```typescript theme={null}
const tx = await merchant.payment.refund(
paymentInfo,
5_000_000n,
'0xTokenCollector...' as `0x${string}`,
'0xSignatureData...' as `0x${string}`,
)
```
**Parameters**
| Name | Type | Description |
| ---------------- | ------------- | ----------------------------------------------------------------------------- |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
| `amount` | `bigint` | Atomic units to refund to the payer |
| `tokenCollector` | `Address` | Token collector that sources the refund (typically `ReceiverRefundCollector`) |
| `collectorData` | `Hex` | Data passed to the collector (for example, the receiver signature) |
**Returns** `Promise`, the refund transaction hash.
Refunds after capture require the merchant to hold enough token balance and to grant an allowance on the refund collector.
## payment.getAmounts
Query the current capturable and refundable amounts for a payment.
```typescript theme={null}
const amounts = await merchant.payment.getAmounts(paymentInfo)
```
**Parameters**
| Name | Type | Description |
| ------------- | ------------- | ----------------------------------- |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
**Returns** `Promise`:
| Field | Type | Description |
| --------------------- | --------- | --------------------------------------------- |
| `hasCollectedPayment` | `boolean` | Whether the on-chain escrow holds the payment |
| `capturableAmount` | `bigint` | Atomic units still capturable from escrow |
| `refundableAmount` | `bigint` | Atomic units still refundable |
## payment.getState
Returns the payment's lifecycle position as a tuple. The SDK exposes no `PaymentState` enum.
```typescript theme={null}
const [hasCollectedPayment, capturableAmount, refundableAmount] =
await merchant.payment.getState(paymentInfo)
```
**Parameters**
| Name | Type | Description |
| ------------- | ------------- | ----------------------------------- |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
**Returns** `Promise`, `[hasCollectedPayment, capturableAmount, refundableAmount]`.
## operator.getConfig
Retrieve all slot addresses from the PaymentOperator contract.
```typescript theme={null}
const config = await merchant.operator.getConfig()
```
**Returns** `Promise`, see `packages/core/src/actions/operator/types.ts`. Key fields:
| Field | Type | Description |
| -------------------------------------- | --------- | ------------------------------ |
| `escrow` | `Address` | Canonical AuthCaptureEscrow |
| `authorizeCondition` / `authorizeHook` | `Address` | Pre/post slots for `authorize` |
| `chargeCondition` / `chargeHook` | `Address` | Pre/post slots for `charge` |
| `captureCondition` / `captureHook` | `Address` | Pre/post slots for `capture` |
| `voidCondition` / `voidHook` | `Address` | Pre/post slots for `void` |
| `refundCondition` / `refundHook` | `Address` | Pre/post slots for `refund` |
| `feeCalculator` | `Address` | Per-operator fee calculator |
| `feeReceiver` | `Address` | Operator fee recipient |
| `protocolFeeConfig` | `Address` | Protocol fee config contract |
## operator.getFeeAddresses
Fetch the fee-related addresses (subset of `getConfig` with both operator and protocol resolved).
```typescript theme={null}
const fees = await merchant.operator.getFeeAddresses()
```
**Returns** `Promise`:
| Field | Type | Description |
| ----------------------- | --------- | ---------------------------- |
| `operatorFeeCalculator` | `Address` | Per-operator calculator |
| `protocolFeeConfig` | `Address` | Protocol fee config contract |
| `protocolFeeCalculator` | `Address` | Protocol-level calculator |
| `operatorFeeRecipient` | `Address` | Where operator fees flow |
| `protocolFeeRecipient` | `Address` | Where protocol fees flow |
## operator.calculateFees
Calculate the full fee breakdown for a payment amount.
```typescript theme={null}
const fees = await merchant.operator.calculateFees(paymentInfo, 1_000_000n)
```
**Parameters**
| Name | Type | Description |
| ------------- | ------------- | ----------------------------------- |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
| `amount` | `bigint` | Atomic units to compute fees for |
**Returns** `Promise`:
| Field | Type | Description |
| ------------------- | -------- | ---------------------------- |
| `protocolFeeBps` | `bigint` | Protocol fee in basis points |
| `operatorFeeBps` | `bigint` | Operator fee in basis points |
| `totalFeeBps` | `bigint` | Sum of the two |
| `protocolFeeAmount` | `bigint` | Atomic units of protocol fee |
| `operatorFeeAmount` | `bigint` | Atomic units of operator fee |
| `totalFeeAmount` | `bigint` | Atomic units of total fee |
| `netAmount` | `bigint` | Amount remaining after fees |
## Capture vs refund decision flow
```mermaid theme={null}
flowchart TD
A[Payment in Escrow] --> B{Check payment.getAmounts}
B --> C{capturableAmount > 0?}
C -->|Yes| D{Has refund request?}
C -->|No| E[Nothing to capture]
D -->|No| F[Safe to capture]
D -->|Yes| G{Approve refund?}
F --> H["payment.capture(paymentInfo, amount)"]
G -->|Yes| I["payment.voidPayment(paymentInfo)"]
G -->|No| J[Deny request, then capture]
J --> H
```
## Next steps
Process incoming refund requests with deny workflows.
Forward escrow settlements to an arbiter service.
Understand the underlying PaymentOperator contract methods.
# Refund handling
Source: https://docs.x402r.org/sdk/merchant/refund-handling
Process, approve, deny, and manage refund requests as a merchant
The merchant client exposes a read-heavy slice of refund actions plus `freeze.isFrozen`. Writes that change refund-request status (`deny`, `refuse`) and writes that lift a freeze (`unfreeze`) live on `createArbiterClient` or on the full `createX402r()` client.
Use `createMerchantClient` for queries below; for executing a refund, see [Capture vs refund decision flow](/sdk/merchant/quickstart#capture-vs-refund-decision-flow). The merchant client's `payment.voidPayment()` flips the request to `Approved` through the `VOID_POST_ACTION_HOOK`.
## Refund request queries
### refund.has
Check whether a refund request exists for a payment.
```typescript theme={null}
const hasRequest = await merchant.refund?.has(paymentInfo)
```
**Parameters**
| Name | Type | Description |
| ------------- | ------------- | ----------------------------------- |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
**Returns** `Promise`.
### refund.getStatus
Retrieve the current status of a refund request.
```typescript theme={null}
import { RefundRequestStatus } from '@x402r/sdk'
const status = await merchant.refund?.getStatus(paymentInfo)
```
**Parameters**
| Name | Type | Description |
| ------------- | ------------- | ----------------------------------- |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
**Returns** `Promise`, `Pending` | `Approved` | `Denied` | `Cancelled` | `Refused`.
### refund.get
Retrieve the complete refund request data.
```typescript theme={null}
const request = await merchant.refund?.get(paymentInfo)
```
**Parameters**
| Name | Type | Description |
| ------------- | ------------- | ----------------------------------- |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
**Returns** `Promise`:
| Field | Type | Description |
| ----------------- | --------------------- | ------------------------------------------- |
| `paymentInfoHash` | `Hex` | keccak256 of the payment info struct |
| `amount` | `bigint` | Amount the payer requested |
| `approvedAmount` | `bigint` | Amount actually executed (0 until approved) |
| `status` | `RefundRequestStatus` | Lifecycle state |
### refund.getByKey
Look up a refund request directly by its payment info hash.
```typescript theme={null}
const request = await merchant.refund?.getByKey(paymentInfoHash)
```
**Parameters**
| Name | Type | Description |
| ----------------- | ----- | ------------------------------------ |
| `paymentInfoHash` | `Hex` | keccak256 of the payment info struct |
**Returns** `Promise`.
## Paginated refund request listing
### refund.getReceiverRequests
Retrieve paginated refund request keys for this merchant (the receiver).
```typescript theme={null}
const { keys, total } = await merchant.refund?.getReceiverRequests(
receiverAddress,
0n,
10n,
) ?? { keys: [], total: 0n }
for (const hash of keys) {
const request = await merchant.refund?.getByKey(hash)
// ... inspect request.amount, request.status
}
```
**Parameters**
| Name | Type | Description |
| ---------- | --------- | -------------------------------------------------- |
| `receiver` | `Address` | Receiver address to query (typically the merchant) |
| `offset` | `bigint` | Index offset |
| `count` | `bigint` | Max entries to return |
**Returns** `Promise<{ keys: readonly Hex[]; total: bigint }>`. To hydrate each entry, call `refund.getByKey(hash)` per key.
`getOperatorRequests` (paginated across all payments under an operator) lives on `createArbiterClient`, not on the merchant client.
## Refund request actions
Approving or denying a request through the operator hook is what the merchant does. Terminal `deny` and `refuse` calls on the RefundRequest contract belong to the arbiter role; from a merchant, execute the refund through `payment.voidPayment()` (which flips the request to `Approved`) or signal a refusal off-chain and let the arbiter terminalize it.
### payment.voidPayment
To approve and execute a refund, call `payment.voidPayment()`. The operator's `VOID_POST_ACTION_HOOK` (RefundRequest) flips the request status to `Approved`.
```typescript theme={null}
const tx = await merchant.payment.voidPayment(paymentInfo)
```
**Parameters**
| Name | Type | Description |
| ------------- | ------------------ | --------------------------------------------- |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
| `data` | `Hex` *(optional)* | Pass-through data for pre/post action plugins |
**Returns** `Promise`.
`voidPayment()` flips the pending RefundRequest to `Approved`. This action cannot be undone.
## Freeze management
### freeze.isFrozen
Check whether a freeze currently holds a payment. The escrow blocks capture on a frozen payment until the arbiter unfreezes it.
```typescript theme={null}
const frozen = await merchant.freeze?.isFrozen(paymentInfo)
```
**Parameters**
| Name | Type | Description |
| ------------- | ------------- | ----------------------------------- |
| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment |
**Returns** `Promise`.
The merchant client exposes `freeze.isFrozen` only. Lifting a freeze (`unfreeze`) is an arbiter-role action; use `createArbiterClient` or `createX402r()`.
## Complete refund workflow
A full workflow that detects a refund request, reviews it, makes a decision, and executes the refund when approved.
```typescript theme={null}
import { createMerchantClient, RefundRequestStatus } from '@x402r/sdk'
import type { PaymentInfo } from '@x402r/sdk'
async function handleRefundWorkflow(
merchant: ReturnType,
paymentInfo: PaymentInfo,
) {
// Step 1: Check if a refund request exists
const hasRequest = await merchant.refund?.has(paymentInfo)
if (!hasRequest) {
console.log('No refund request for this payment')
return
}
// Step 2: Get the full request data
const request = await merchant.refund?.get(paymentInfo)
console.log('Refund request:', request?.amount, 'status:', request?.status)
// Step 3: Only process pending requests
if (request?.status !== RefundRequestStatus.Pending) {
console.log('Request already processed')
return
}
// Step 4: Check if the payment is frozen
const frozen = await merchant.freeze?.isFrozen(paymentInfo)
if (frozen) {
console.log('Payment is frozen, resolve dispute first')
return
}
// Step 5: Check available amounts
const amounts = await merchant.payment.getAmounts(paymentInfo)
console.log('Available to refund:', amounts.refundableAmount)
// Step 6: Make a decision
const shouldApprove = request.amount <= amounts.refundableAmount
if (shouldApprove) {
// Execute the refund (the VOID_POST_ACTION_HOOK flips the request to Approved)
const tx = await merchant.payment.voidPayment(paymentInfo)
console.log('Refund executed:', tx)
} else {
// The arbiter can terminalize the request via refund.deny / refund.refuse.
// The merchant can simply leave the request Pending and capture as usual,
// or escalate off-chain to the arbiter.
console.log('Declining; arbiter may deny if escalated')
}
}
```
## Refund request lifecycle
```mermaid theme={null}
sequenceDiagram
participant P as Payer
participant R as RefundRequest Contract
participant M as Merchant
participant O as PaymentOperator
P->>R: refund.request(paymentInfo, amount)
R-->>M: RefundRequested event
M->>R: refund.has(paymentInfo)
R-->>M: true
M->>R: refund.get(paymentInfo)
R-->>M: RefundRequestData
M->>M: Review request (policy check)
alt Approve
M->>O: payment.voidPayment(paymentInfo)
O->>P: Funds returned to payer
else Decline (off-chain) / escalate to arbiter
Note over P,R: Arbiter may terminalize via refund.deny / refund.refuse
end
```
## Method reference
| Method | Parameters | Returns |
| ----------------------------- | -------------------------- | -------------------------------------------------------- |
| `refund.has` | `paymentInfo` | `boolean` |
| `refund.getStatus` | `paymentInfo` | `RefundRequestStatus` |
| `refund.get` | `paymentInfo` | `RefundRequestData` |
| `refund.getByKey` | `paymentInfoHash` | `RefundRequestData` |
| `refund.getStoredPaymentInfo` | `paymentInfoHash` | `PaymentInfo` |
| `refund.getReceiverRequests` | `receiver, offset, count` | `{ keys: readonly Hex[]; total: bigint }` |
| `refund.getCancelCount` | `paymentInfo` | `bigint` (number of cancellations on this RefundRequest) |
| `refund.getCancelledAmount` | `paymentInfo, cancelIndex` | `bigint` (amount cancelled at the given index) |
| `freeze.isFrozen` | `paymentInfo` | `boolean` |
| `payment.voidPayment` | `paymentInfo, data?` | `Hash` (flips the pending RefundRequest to `Approved`) |
## Next steps
Capture funds, charge, and query escrow state.
RefundRequest contract details and state machine.
# Overview
Source: https://docs.x402r.org/sdk/overview
TypeScript SDK for adding escrow, refunds, and dispute resolution to x402 payments
The X402r SDK is in active development. APIs may change between releases. Always test on Base Sepolia before using real funds on mainnet.
Three roles interact with the protocol:
* **Merchants** receive payments into escrow and capture funds after delivery
* **Payers** can request refunds, freeze payments, and submit evidence during disputes
* **Arbiters** verify transactions or resolve disputes (two models below)
### Two Operator Models
**Marketplace** (`deployMarketplaceOperator`): The merchant releases funds after escrow. If the payer contests, they file a refund request and an arbiter resolves it. Use this for general commerce where most transactions clear without dispute.
**Delivery Protection** (`deployDeliveryProtectionOperator`): The arbiter evaluates every transaction. The arbiter or a satisfied payer can capture funds. On a FAIL verdict, the arbiter can trigger an immediate refund. If nobody acts, funds return to the payer once escrow expires. Use this for AI content verification, schema validation, or quality checks.
See [Deploy an operator](/sdk/deploy-operator) for the full preset feature comparison, slot configurations, and deployment code.
### Packages
```bash npm theme={null}
npm install @x402r/sdk
```
```bash pnpm theme={null}
pnpm add @x402r/sdk
```
```bash bun theme={null}
bun add @x402r/sdk
```
`@x402r/sdk` is the only package most developers need. It includes role-scoped client factories, 8 action groups (payment, escrow, refund, evidence, freeze, query, operator, watch), an `.extend()` plugin system, and ERC-8004 helpers that extract on-chain identity and reputation data from x402 extension responses.
For low-level access to contract ABIs and deploy utilities:
```bash npm theme={null}
npm install @x402r/core
```
```bash pnpm theme={null}
pnpm add @x402r/core
```
```bash bun theme={null}
bun add @x402r/core
```
For x402 server integration:
```bash npm theme={null}
npm install @x402r/helpers
```
```bash pnpm theme={null}
pnpm add @x402r/helpers
```
```bash bun theme={null}
bun add @x402r/helpers
```
For one-shot payments from the command line (no project install required):
```bash npm theme={null}
npx @x402r/cli pay [options]
```
```bash pnpm theme={null}
pnpm dlx @x402r/cli pay [options]
```
```bash bun theme={null}
bunx @x402r/cli pay [options]
```
### Guides
Capture funds from escrow, charge directly, void, and process refunds using `createMerchantClient`.
Deploy a PaymentOperator on Base or Base Sepolia and configure plugin slots.
Wire merchant settlements into an arbiter that gates capture on response quality.
Wallet-agnostic one-shot payments from the command line or scripts.
## Network support
All x402r-authored contracts use universal CREATE2 addresses: every supported chain resolves to the same address as every other supported chain.
Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. More EVM chains land as canonical `base/commerce-payments@v1.0.0` coverage extends.
| Chain | Chain ID | USDC |
| ------------ | -------- | -------------------------------------------- |
| Base | `8453` | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
| Base Sepolia | `84532` | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
### commerce-payments v1 primitives
The `base/commerce-payments@v1.0.0` primitives ship at canonical CREATE2 addresses via CreateX permissionless salts. Each contract resolves to the same address on every supported chain.
| Contract | Address |
| ------------------------- | -------------------------------------------- |
| `AuthCaptureEscrow` | `0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff` |
| `ERC3009PaymentCollector` | `0x0E3dF9510de65469C4518D7843919c0b8C7A7757` |
| `Permit2PaymentCollector` | `0x992476B9Ee81d52a5BdA0622C333938D0Af0aB26` |
Salt namespace: `commerce-payments::v1::`.
Import the addresses from `@x402r/core`:
```ts theme={null}
import { authCaptureEscrow, tokenCollector } from '@x402r/core';
// AuthCaptureEscrow, canonical across every supported chain.
authCaptureEscrow;
// Primary token collector (currently aliases ERC3009PaymentCollector).
tokenCollector;
```
The SDK exposes these primitives on the chains listed in `@x402r/core`'s `x402rChains` (Base + Base Sepolia today). CreateX salts already reserve the CREATE2 addresses on more chains, and the registry enables each one as canonical `base/commerce-payments@v1.0.0` coverage extends.
# auth-capture Scheme
Source: https://docs.x402r.org/x402-integration/auth-capture/index
Concept, flow diagrams, and captureAuthorizer model for the x402 auth-capture payment scheme
## Overview
The **`auth-capture` scheme** for x402 v2 uses the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors) directly, no fork. The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it, either locking funds in escrow for later capture (two-phase) or sending them directly to the receiver with refund capability (single-shot).
Unlike `exact`, which has no mechanism for returning funds, `auth-capture` supports returning funds to the client through void, refund, and reclaim.
## Settlement Paths
The scheme supports two settlement paths, selected via `extra.autoCapture`:
| `autoCapture` | Behavior |
| :---------------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| `false` (default) | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, or refund. Client can reclaim if the capture deadline passes. |
| `true` | Single-shot. Funds sent directly to the receiver. CaptureAuthorizer can refund post-settlement. |
### Two-phase (`autoCapture: false`, default)
```
AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND)
```
The facilitator submits the client's authorization, locking funds in escrow via `AuthCaptureEscrow.authorize()`. The token collector executes the client's signature (ERC-3009 `receiveWithAuthorization` or Permit2 `permitTransferFrom`) to pull tokens into escrow.
Server returns the resource (HTTP 200).
The captureAuthorizer can capture (capture funds to the receiver) or void (return escrowed funds to the client). Capture conditions are policy-defined per captureAuthorizer (time-locked, arbiter-approved, etc.).
If `captureDeadline` passes without capture, the client can reclaim funds directly from the escrow without captureAuthorizer involvement.
After capture, the captureAuthorizer can refund within the `refundDeadline` window.
### Single-shot (`autoCapture: true`)
```
CHARGE -> RESOURCE DELIVERED -> (REFUND)
```
The facilitator submits the client's authorization, sending funds directly to the receiver via `AuthCaptureEscrow.charge()`. No escrow hold.
Server returns the resource (HTTP 200).
The captureAuthorizer can refund within the `refundDeadline` window.
No capture, void, or reclaim, funds are never held in escrow.
## Visual Flow
### Exact Payment (Immediate Settlement)
```mermaid theme={null}
sequenceDiagram
participant Client
participant Server
participant Receiver
Client->>Server: 1. Payment + Signature
Server->>Receiver: 2. Immediate Transfer
Server->>Client: 3. Deliver Resource
Note over Client,Receiver: No recourse after payment - Payment is final
```
### auth-capture (Two-phase)
```mermaid theme={null}
sequenceDiagram
participant Client
participant Server
participant Facilitator
participant Escrow as AuthCaptureEscrow
participant Receiver
Client->>Server: GET /resource
Server-->>Client: 402 PaymentRequired
Note over Client: Signs ERC-3009 or Permit2
Client->>Server: PaymentPayload with signature
Server->>Facilitator: verify + settle
Facilitator->>Escrow: authorize(paymentInfo, amount, tokenCollector, signature)
Escrow->>Escrow: Lock funds
Facilitator-->>Server: Settlement confirmed
Server-->>Client: 200 OK + resource
Note over Facilitator,Escrow: Later: captureAuthorizer acts based on policy
alt Successful completion
Facilitator->>Escrow: capture(paymentInfo, amount, feeBps, feeReceiver)
Escrow->>Receiver: Transfer funds (minus fees)
else Void (full return from escrow)
Facilitator->>Escrow: void(paymentInfo)
Escrow->>Client: Return to payer
else Capture deadline passed
Client->>Escrow: reclaim(paymentInfo)
Escrow->>Client: Return to payer (no captureAuthorizer needed)
end
```
### Key Differences
| Aspect | Exact | auth-capture |
| --------------------- | --------------------------- | ------------------------------------------------------------ |
| **Settlement** | Immediate on request | Via escrow (two-phase) or direct with refund (single-shot) |
| **Payer Protection** | None (payment final) | Refundable in both paths |
| **Resource Delivery** | After payment clears | Immediately after authorization |
| **Recourse** | No recourse | Reclaim after capture deadline, refund via captureAuthorizer |
| **Fee System** | None | Configurable (min/max bounds, client-signed) |
| **Use Case** | Trusted, low-value, instant | High-value, variable cost, disputes |
## CaptureAuthorizer
The **captureAuthorizer** is the address that may call `authorize`, `capture`, `void`, `refund`, or `charge` on a payment. The escrow contract gates those operations on `msg.sender`. In x402's facilitator-submits flow that means either the facilitator's EOA, or any smart contract that ends up calling the escrow (for example, an arbiter contract with dispute logic or a multisig).
| Use Case | CaptureAuthorizer |
| ---------------------- | ----------------------------------------------------------- |
| Session billing | EOA that tracks usage off-chain, captures periodically |
| Time-locked escrow | Contract that releases after a period expires |
| Dispute resolution | Arbiter contract that decides capture vs refund |
| Immediate (exact-like) | Facilitator with `autoCapture: true` for instant settlement |
| Streaming payments | Contract that performs time-proportional captures |
## vs Exact Scheme
The `auth-capture` scheme adds an authorization step before settlement (or refundability for single-shot). For simple immediate payments where trust and refundability aren't concerns, the `exact` scheme remains more efficient.
## Next Steps
PaymentRequirements and PaymentPayload shapes for EIP-3009 and Permit2.
The 13-step verification flow, settlement logic, and error codes.
On-chain struct, expiry ordering, and safety guarantees.
Build your first auth-capture payment flow.
## References
* [Commerce Payments Protocol](https://blog.base.dev/commerce-payments-protocol)
* [AuthCaptureEscrow Contract](https://github.com/base/commerce-payments)
* [EIP-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009)
* [Uniswap Permit2](https://docs.uniswap.org/contracts/permit2/overview)
* [auth-capture client scheme (`@x402/evm/auth-capture/client`)](https://github.com/x402-foundation/x402/tree/main/typescript/packages/mechanisms/evm/src/auth-capture)
* [x402r auth-capture Scheme Reference Implementation](https://github.com/BackTrackCo/x402r-scheme)
# PaymentInfo Struct
Source: https://docs.x402r.org/x402-integration/auth-capture/payment-info
On-chain PaymentInfo struct, expiry ordering, and safety guarantees
This is the on-chain Solidity struct. The JSON payload omits the `payer` field; the facilitator recovers it from the signature at settlement time. Wire-format `extra` uses spec-level field names; the on-chain struct keeps canonical names so the EIP-712 typehash matches the AuthCaptureEscrow contract byte-for-byte.
```solidity theme={null}
struct PaymentInfo {
address operator; // = extra.captureAuthorizer
address payer; // payload-derived
address receiver; // = requirements.payTo
address token; // = requirements.asset
uint120 maxAmount; // = requirements.amount
uint48 preApprovalExpiry; // = now + maxTimeoutSeconds (client-derived)
uint48 authorizationExpiry; // = extra.captureDeadline
uint48 refundExpiry; // = extra.refundDeadline
uint16 minFeeBps;
uint16 maxFeeBps;
address feeReceiver; // = extra.feeRecipient
uint256 salt; // = payload.salt (client-generated, fresh per request)
}
```
## Expiry Ordering
The contract enforces: `preApprovalExpiry <= authorizationExpiry <= refundExpiry`
| Expiry | Wire field | Enforced At | Effect |
| --------------------- | ----------------- | -------------------------- | ---------------------------------- |
| `preApprovalExpiry` | derived | `authorize()` / `charge()` | Blocks settlement after this time |
| `authorizationExpiry` | `captureDeadline` | `capture()` | Blocks capture; allows `reclaim()` |
| `refundExpiry` | `refundDeadline` | `refund()` | Blocks refund requests |
## Safety Guarantees
The escrow contract enforces invariants on-chain:
The client-signed `maxAmount` caps the settlement amount. Attempts to exceed the limit revert.
Each payment has a unique nonce derived from `(chainId, escrowAddress, paymentInfoHash)`. The nonce is consumed on-chain at settlement.
After `captureDeadline`, the payer can reclaim escrowed funds directly without captureAuthorizer approval.
Min/max fee bounds in `PaymentInfo` are client-signed and enforced on-chain. The captureAuthorizer must respect these limits.
**CaptureAuthorizer Trust Required:** The captureAuthorizer controls when and how much to capture. Choose with intent and understand the capture policy. See [PaymentOperator](/contracts/payment-operator) for examples.
## Next Steps
Where each PaymentInfo field comes from on the wire.
The 13-step verification flow that enforces these invariants.
# Verification and Settlement
Source: https://docs.x402r.org/x402-integration/auth-capture/verification-and-settlement
Facilitator verification flow, settlement logic, EIP-6492 wallets, and error codes
## Verification Logic
The facilitator performs these checks in order:
1. **Type guard**: Payload matches `Eip3009Payload` or `Permit2Payload` (includes `signature` and `salt`).
2. **Scheme match**: `requirements.scheme === "auth-capture"` and `payload.accepted.scheme === "auth-capture"`.
3. **Network match**: `payload.accepted.network === requirements.network` and format is `eip155:`.
4. **Extra validation**: All required `extra` fields present.
5. **Method routing**: `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape.
6. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and the payload's `validBefore` (EIP-3009) or `deadline` (Permit2) `<= captureDeadline`.
7. **Time window**: `validBefore` / `deadline > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only).
8. **Spender / collector match**: `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2).
9. **Token match**: `permit2Authorization.permitted.token === requirements.asset` (Permit2 only, EIP-3009 binds via signing domain).
10. **Signature verify**: Recover signer from EIP-712 (`ReceiveWithAuthorization` or `PermitTransferFrom`); must match payer.
11. **Amount**: Authorization amount matches `requirements.amount`.
12. **Nonce match**: Reconstruct `PaymentInfo` from extra + salt + payer + requirements; recompute the payer-agnostic hash; assert it matches the wire nonce. This transitively enforces equality on every field encoded in `PaymentInfo` (receiver, token, deadlines, fee bounds, feeRecipient).
13. **Simulate**: Call `AuthCaptureEscrow.authorize(...)` or `.charge(...)` via `eth_call` to verify success.
The `SAFETY_MARGIN_SECONDS` constant is `6`, which is why deadline comparisons use `now + 6s`.
### EIP-6492 Support
For smart wallet clients, the signature may be EIP-6492 wrapped (containing deployment bytecode). The facilitator extracts the inner ECDSA signature for verification. The on-chain `ERC6492SignatureHandler` in the token collector handles wallet deployment during settlement.
## Settlement Logic
1. **Re-verify** the payload (catch expired/invalid payloads before spending gas).
2. **Determine function**: `extra.autoCapture === true ? "charge" : "authorize"`.
3. **Resolve collector**: `EIP3009_TOKEN_COLLECTOR_ADDRESS` or `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (per `assetTransferMethod`).
4. **Encode `collectorData`**: raw ERC-3009 signature, or ABI-encoded Permit2 signature.
5. **Call escrow**: `AuthCaptureEscrow.(paymentInfo, amount, tokenCollector, collectorData)`.
6. **Wait for receipt**: 60s timeout.
7. **Return result**: tx hash, network, payer.
## Error Codes
### Verification Errors
| Error Code | Description |
| ----------------------------------- | --------------------------------------------------------------------------------- |
| `invalid_payload_format` | Payload doesn't match `Eip3009Payload` or `Permit2Payload`. |
| `unsupported_scheme` | Scheme is not `auth-capture`. |
| `network_mismatch` | Payload network doesn't match requirements. |
| `invalid_network` | Network format is not `eip155:`. |
| `invalid_auth_capture_extra` | Extra is missing required fields. |
| `unsupported_asset_transfer_method` | `assetTransferMethod` is not `"eip3009"` or `"permit2"`. |
| `payload_method_mismatch` | Payload shape doesn't match `assetTransferMethod`. |
| `capture_deadline_expired` | `captureDeadline <= now + 6s`. |
| `invalid_deadline_ordering` | Deadlines violate `now + maxTimeoutSeconds <= captureDeadline <= refundDeadline`. |
| `authorization_expired` | EIP-3009 `validBefore` (or Permit2 `deadline`) `<= now + 6s`. |
| `authorization_not_yet_valid` | EIP-3009 `validAfter > now`. |
| `invalid_auth_capture_signature` | Signature verification failed. |
| `amount_mismatch` | Authorization value doesn't match `requirements.amount`. |
| `token_collector_mismatch` | `to` / `spender` doesn't match the canonical collector for the method. |
| `token_mismatch` | Permit2 `permitted.token` doesn't match `requirements.asset`. |
| `nonce_mismatch` | Wire nonce doesn't match the recomputed payer-agnostic `PaymentInfo` hash. |
| `insufficient_balance` | Payer balance is less than required amount. |
| `simulation_failed` | Settlement simulation reverted with an unmapped error. |
### Settlement Errors
| Error Code | Description |
| ---------------------- | ------------------------------------------------- |
| `verification_failed` | Re-verification before settlement failed. |
| `transaction_reverted` | On-chain transaction reverted after confirmation. |
## Next Steps
PaymentRequirements and PaymentPayload shapes.
On-chain struct, expiry ordering, and safety guarantees.
# Wire Format
Source: https://docs.x402r.org/x402-integration/auth-capture/wire-format
PaymentRequirements, PaymentPayload, and Extra-field reference for the auth-capture scheme
## PaymentRequirements (402 Response)
Server sends this to request payment:
```json theme={null}
{
"x402Version": 2,
"accepts": [{
"scheme": "auth-capture",
"network": "eip155:8453",
"amount": "1000000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xReceiverAddress",
"maxTimeoutSeconds": 60,
"extra": {
"name": "USDC",
"version": "2",
"captureAuthorizer": "0xCaptureAuthorizerAddress",
"captureDeadline": 1740758554,
"refundDeadline": 1741276954,
"minFeeBps": 0,
"maxFeeBps": 1000,
"feeRecipient": "0xFeeRecipientAddress",
"autoCapture": false,
"assetTransferMethod": "eip3009"
}
}]
}
```
A server MAY list more than one `accepts[]` entry with different `assetTransferMethod` values so clients can pick the method matching their token approvals.
## Signing the payload (client)
Clients do not hand-build these payloads. The client half of the scheme ships in the x402 monorepo as `AuthCaptureEvmScheme` on the `@x402/evm/auth-capture/client` subpath. Register it on an `x402Client` and it reads the `extra` fields, reconstructs the PaymentInfo struct, derives the payer-agnostic nonce, and emits the ERC-3009 (default) or Permit2 payload shown below.
```typescript theme={null}
import { AuthCaptureEvmScheme } from '@x402/evm/auth-capture/client'
import { x402Client, wrapFetchWithPayment } from '@x402/fetch'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`)
const client = new x402Client()
client.register('eip155:*', new AuthCaptureEvmScheme(account))
// fetchWithPayment auto-signs any auth-capture 402 it receives
const fetchWithPayment = wrapFetchWithPayment(fetch, client)
```
The signer only needs `address` and `signTypedData`, so a bare viem `LocalAccount` works with no `PublicClient`. The scheme selects ERC-3009 or Permit2 from `extra.assetTransferMethod`.
## PaymentPayload: EIP-3009 (default)
Client sends this with a signed ERC-3009 authorization:
```json theme={null}
{
"x402Version": 2,
"resource": {
"url": "https://api.example.com/resource",
"method": "GET"
},
"accepted": {
"scheme": "auth-capture",
"network": "eip155:8453",
"amount": "1000000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xReceiverAddress",
"maxTimeoutSeconds": 60,
"extra": { "..." }
},
"payload": {
"authorization": {
"from": "0xPayerAddress",
"to": "0xEIP3009TokenCollectorAddress",
"value": "1000000",
"validAfter": "0",
"validBefore": "1740675754",
"nonce": "0xf374...3480"
},
"signature": "0x2d6a...571c",
"salt": "0x0000000000000000000000000000000000000000000000000000000000000abc"
}
}
```
## PaymentPayload: Permit2
When `extra.assetTransferMethod === "permit2"`, the client signs a Permit2 `PermitTransferFrom`:
```json theme={null}
{
"x402Version": 2,
"resource": { "url": "https://api.example.com/resource", "method": "GET" },
"accepted": { "scheme": "auth-capture", "...": "..." },
"payload": {
"permit2Authorization": {
"from": "0xPayerAddress",
"permitted": {
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "1000000"
},
"spender": "0xPermit2TokenCollectorAddress",
"nonce": "11021048692073456...",
"deadline": "1740675754"
},
"signature": "0x2d6a...571c",
"salt": "0x0000000000000000000000000000000000000000000000000000000000000abc"
}
}
```
The deterministic nonce binds the merchant address (no witness struct).
## Field Reference
### Required Extra Fields
| Field | Type | Description |
| ------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------- |
| `name` | string | EIP-712 token-domain name (for example, `"USDC"`). Used for ERC-3009 signing only. |
| `version` | string | EIP-712 token-domain version (for example, `"2"`). |
| `captureAuthorizer` | address | Address that may call `authorize`, `capture`, `void`, `refund`, or `charge`. Committed on-chain as `PaymentInfo.operator`. |
| `captureDeadline` | uint48 | Absolute Unix seconds: capture must occur before this. Encoded as `authorizationExpiry`. |
| `refundDeadline` | uint48 | Absolute Unix seconds: refunds allowed until this. Encoded as `refundExpiry`. |
| `feeRecipient` | address | Fee recipient. Set to `address(0)` to let the captureAuthorizer specify any non-zero recipient at capture/charge time. |
| `minFeeBps` | uint16 | Lowest fee in basis points the captureAuthorizer must take. `0` = no floor. |
| `maxFeeBps` | uint16 | Highest fee in basis points the captureAuthorizer can take. |
### Optional Extra Fields
| Field | Type | Description | Default |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------ | ----------- |
| `autoCapture` | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). | `false` |
| `assetTransferMethod` | `"eip3009"` \| `"permit2"` | Which token collector to use. | `"eip3009"` |
**Fee Configuration:** The escrow enforces fees on-chain via the `PaymentInfo` struct. The escrow rejects captures/charges that fall outside `[minFeeBps, maxFeeBps]`. If `feeRecipient` is non-zero, the actual fee recipient at capture/charge must match.
## Nonce Derivation
The signature nonce is the payer-agnostic `PaymentInfo` hash. The encoding zeros out the payer; every other field carries the value that will appear on-chain.
```
paymentInfoHash = keccak256(abi.encode(PAYMENT_INFO_TYPEHASH, paymentInfoWithZeroPayer))
nonce = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, paymentInfoHash))
```
The `salt` field enforces freshness: each signing call generates a fresh `bytes32` salt, so two payers signing concurrently produce distinct nonces with no collision risk.
## Next Steps
The 13-step verification flow and error codes.
How wire fields map to the on-chain struct.
# X402 Protocol Overview
Source: https://docs.x402r.org/x402-integration/overview
How x402r extends the x402 HTTP payment protocol with escrow capabilities
## What is X402?
**X402** is an HTTP payment protocol that uses the `402 Payment Required` status code to enable machine-to-machine payments. It allows servers to request payment from clients using standardized headers and payload formats.
Think of it as "Stripe for the programmable internet" - agents, robots, and autonomous systems can pay for API access, compute time, or any HTTP resource.
## Payment Schemes
X402 v2 supports two payment schemes:
Immediate settlement - payment clears the moment the client sends the request.
**Best for:** Simple purchases, low-value transactions, trusted services
Deferred settlement - funds stay locked until conditions clear.
**Best for:** High-value transactions, usage-based billing, long-running tasks
## Why Escrow
The `exact` scheme works well for immediate-delivery payments, but creates friction for:
### High-Value Transactions
**Problem:** No recourse if service fails after payment
```
Client pays $500 → Server crashes → Money lost
```
**Escrow Solution:** Escrow holds funds until the captureAuthorizer verifies the work
```
Client authorizes $500 → Work completes → Operator releases → Server receives
```
### Variable Pricing
**Problem:** Usage-based billing requires estimating upfront
Consider an LLM agent making API calls:
* Unknown final cost (depends on tokens used)
* Can't pay exact amount in advance
* Server needs guarantee of payment
**Escrow Solution:** Lock a max amount, capture actual usage
```
Client authorizes $10 → Uses $6.50 → Operator captures $6.50 → Refund $3.50
```
### Long-Running Tasks
**Problem:** Work takes hours or days to complete
```
Client pays for video rendering → 48 hours later → How to verify completion?
```
**Escrow Solution:** Conditional capture with verification
```
Client authorizes → Work progresses → Client verifies → Capture on approval
```
### Multi-Request Sessions
**Problem:** Signing 1,000 individual requests is impractical
```
Agent makes 1,000 API calls at $0.01 each
= 1,000 signatures + 1,000 on-chain transactions
```
**Escrow Solution:** One authorization, many captures
```
Client authorizes $10 once → Server tracks usage → Server batches captures on a schedule
```
## How x402r Extends X402
x402r provides the **auth-capture scheme implementation** for x402:
1. **Base Commerce Payments Integration**
* Audited escrow contracts from Base
* Auth/capture pattern for deferred settlement
* On-chain safety guarantees
2. **Operator Contracts**
* Conditional capture logic
* Dispute resolution
* Fee distribution
* Time-based capture
3. **Payment Facilitator**
* Validates ERC-3009 signatures
* Settles authorizations on-chain
* Tracks payment state
4. **Developer Tools**
* TypeScript SDK
* Deployment scripts
* Example implementations
## Payment Flow
The auth-capture scheme is two-phase: the facilitator first authorizes (locks the client's signed funds in escrow at the 402 settlement), then later captures to the receiver or voids back to the payer per policy. For the full HTTP and on-chain sequence diagrams, see the [auth-capture flow](/x402-integration/auth-capture) and the [on-chain payment sequence](/contracts/architecture#payment-flow-sequence).
## Use Cases
LLM agent needs to call external APIs with variable token costs.
**Flow:**
1. Agent authorizes \$20 max
2. Makes 50 API calls totaling \$12.50
3. Server captures \$12.50
4. Agent reclaims unused \$7.50
Client needs GPU cluster for training job.
**Flow:**
1. Client authorizes \$500 for 48-hour job
2. Training completes in 36 hours (\$375)
3. Client verifies results
4. Operator releases $375, refunds $125
Application needs access to real-time data feed.
**Flow:**
1. App authorizes \$100 for monthly access
2. Provider streams data
3. Provider captures \$3.33 daily (30-day billing)
4. Automatic refund if service interrupted
Client hires developer for project work.
**Flow:**
1. Client authorizes \$2000 in escrow
2. Developer completes milestones
3. Arbiter verifies each milestone
4. Operator releases payment on approval
5. Dispute resolution if disagreement
## Key Concepts
### Authorization
Lock funds in escrow without immediate transfer. Client signs an ERC-3009 authorization allowing the escrow contract to pull tokens.
### Capture
Capture authorized funds to the receiver. The operator contract decides when and how much to capture based on configured conditions.
### Void
Return funds to payer before capture. Used for full refunds during the escrow period.
### Reclaim
Safety valve for payer. If authorization expires without capture, payer can reclaim funds directly from escrow.
### Operator
Smart contract that controls capture/void logic. Different operators enable different payment patterns:
* **Time-locked**: Capture after period expires
* **Arbiter-controlled**: Third party decides capture
* **Usage-based**: Capture proportional to consumption
* **Immediate**: Behaves like `exact` scheme
## Next Steps
Complete technical specification for the auth-capture payment scheme.
Understand the escrow and operator contracts.
Get started building with x402r.