Skip to main content
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:
PresetUse caseFreezeFeesRefundRequest
deployMarketplaceOperatorGeneral marketplace with dispute resolutionYes (optional)Yes (optional)Yes
deployDeliveryProtectionOperatorGarbage detection / delivery verificationNoNoNo
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)
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.
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

OptionTypeDescription
chainIdnumberTarget chain ID (for example, 84532 for Base Sepolia)
feeReceiverAddressAddress that receives operator fees
arbiterAddressArbiter address for dispute resolution
escrowPeriodSecondsbigintEscrow waiting period (for example, 604800n for 7 days)
freezeDurationSecondsbigintHow long freezes last. Default: 0n (permanent until unfrozen)
operatorFeeBpsbigintFee in basis points. Default: 0n (no fee). 100n = 1%
authorizedCodehashHexOptional. Restricts which contract codehashes can record. Defaults to bytes32(0) (no restriction)

Deployment result

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)

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:
SlotContractPurpose
AUTHORIZE_PRE_ACTION_CONDITION(none)Default: anyone with a valid signature can authorize
AUTHORIZE_POST_ACTION_HOOKEscrowPeriodRecords authorization timestamp
CHARGE_PRE_ACTION_CONDITION(none)No restrictions on charge
CAPTURE_PRE_ACTION_CONDITIONEscrowPeriod (or AND(EscrowPeriod, Freeze) if freeze enabled)Blocks capture during escrow period
VOID_PRE_ACTION_CONDITIONOR(Receiver, Arbiter)Receiver or arbiter can approve
VOID_POST_ACTION_HOOKRefundRequestTracks refund request state
REFUND_PRE_ACTION_CONDITIONReceiverOnly receiver after escrow
FEE_CALCULATORStaticFeeCalculatorFixed percentage fee (if configured)
FEE_RECEIVERYour addressReceives fees

Network support

The deploy presets target the chains in @x402r/core’s x402rChains (Base and Base Sepolia today). See 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.

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.
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)
OptionTypeDescription
chainIdnumberTarget chain
arbiterAddressArbiter address for capture and refund decisions
feeReceiverAddressReceives protocol fees
escrowPeriodSecondsbigintVerification window before automatic refund
authorizedCodehashHexOverride the default hookCombinatorCodehash. Optional
paymentIndexRecorderHookAddressAddressOverride the default PaymentIndexRecorderHook. Pass zeroAddress to skip on-chain payment indexing. Optional
allowArbiterRefundbooleanLets the arbiter refund immediately during escrow. Default: false
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:
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)
SlotContractPurpose
CAPTURE_PRE_ACTION_CONDITIONOrCondition([SAC(arbiter), PayerCondition])Arbiter or satisfied payer can capture
AUTHORIZE_POST_ACTION_HOOKHookCombinator([EscrowPeriod, PaymentIndexRecorderHook])Records authorization time and indexes payments on-chain
VOID_PRE_ACTION_CONDITIONOrCondition([EscrowPeriod, ReceiverCondition, SAC(arbiter)])Escrow expiry, receiver voluntary refund, or arbiter immediate refund
REFUND_PRE_ACTION_CONDITIONReceiverConditionOnly receiver after escrow

Next Steps

Merchant Guide

Accept payments, capture funds from escrow.

Examples

See working merchant and client examples.

forwardToArbiter()

Forward escrow settlements to an arbiter service.

Smart Contracts

On-chain architecture, conditions, and hooks.