Skip to content

[Extension Proposal] Escrow/Refund Setup With Only Extensions! #864

@A1igator

Description

@A1igator

x402r: Refundable Payments for x402

A secure, transparent refund system for x402 payments. Enable dispute resolution and buyer protection with minimal code changes.


Overview

When agents or users pay for services via x402, things can go wrong:

  • An API returns empty data after payment
  • A purchased item arrives damaged or wrong
  • A merchant becomes unresponsive

x402r solves this by routing payments through escrow contracts, enabling trustless refunds and dispute resolution.


How It Works

┌─────────────────────────────────────────────────────────────────────────┐
│                    STANDARD x402 PAYMENT                                │
│                                                                         │
│   Client ──────────────────────────────────────────────► Merchant       │
│                    pays directly (no recourse)                          │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│                    x402r REFUNDABLE PAYMENT                             │
│                                                                         │
│   Client ────► Proxy ────► Escrow ────► Merchant                        │
│                  │            │                                         │
│                  │            └─ Holds funds until dispute period ends  │
│                  │               If disputed:  Arbiter decides          │
│                  │                                                      │
│                  └─ Deterministic address (computed, not deployed)      │
│                     Client sees no difference!                          │
└─────────────────────────────────────────────────────────────────────────┘

The Flow

  1. Merchant registers with a shared escrow contract and deploys their relay proxy at registration (one-time setup)
  2. Server marks routes as refundable → payTo is swapped to a proxy address
  3. Client pays to proxy address (no code changes needed)
  4. Facilitator routes payment through escrow
  5. Escrow holds funds for dispute period
  6. Disputes can be raised; arbiter resolves them

Key Concepts

Deterministic Proxy Addresses

Each merchant gets a unique proxy address computed using CREATE3 via CreateX:

proxyAddress = CREATE3(salt) // via CreateX
where salt = keccak256(merchantPayout + factoryAddress)

Why this matters:

Property Benefit
Deterministic Same inputs → same address, always
Computed locally No blockchain calls needed on server
No bytecode needed CREATE3 doesn't require contract bytecode to compute address in comparison to CREATE2

The Proxy Pattern

Instead of deploying a full contract per merchant (expensive), we use proxies:

┌─────────────────────────────────┐
│  Shared Implementation          │  ← One copy of the logic (deployed once)
│  (DepositRelay)                 │
└─────────────────────────────────┘
              ▲
              │ delegatecall
              │
┌─────────────────────────────────┐
│  Merchant Proxy                 │  ← Small contract per merchant (~1KB)
│  (RelayProxy)                   │
│                                 │
│  Stores as immutables:          │
│  - MERCHANT_PAYOUT              │
│  - TOKEN                        │
│  - ESCROW                       │
│  - IMPLEMENTATION               │
└─────────────────────────────────┘

Cost comparison:

  • Without proxies: 50KB × N merchants = expensive
  • With proxies: 50KB (shared) + 1KB × N merchants = cheap

Integration Guide

For Merchants

Step 1: Register with escrow and deploy your relay proxy

Register your payout address with a shared escrow contract and deploy your relay proxy via the contracts.

Step 2: Install the package

npm install @x402/extensions

Step 3: Mark routes as refundable

import { refundable, withRefund } from "@x402/extensions/refund";

const routes = {
  "/api/premium": {
    accepts: refundable({
      scheme: "exact",
      payTo: "0xYourMerchantPayout.. .",
      price: "$0.01",
      network: "eip155:84532",
    }),
  },
  "/api/basic": {
    // Not wrapped in refundable() → pays directly to merchant
    accepts:  {
      scheme: "exact",
      payTo: "0xYourMerchantPayout...",
      price: "$0.001",
      network:  "eip155:84532",
    },
  },
};

// Transform refundable routes
const processedRoutes = withRefund(routes, FACTORY_ADDRESS);

What happens under the hood:

  1. refundable() marks the payment option and stores the original payTo
  2. withRefund() processes each route:
    • Computes deterministic proxy address via CREATE3 using CreateX
    • Replaces payTo with proxy address
    • Adds extensions.refund with proxy factory address
  3. Client signs payment to proxy address (no changes needed)
  4. Extension flows through: Server → Client → Facilitator

For Facilitators

Step 1: Install the package

npm install @x402/extensions

Step 2: Add the refund hook

import { settleWithRefundHelper } from "@x402/extensions/refund";

// Settle hook - routes payment through escrow (proxy must be deployed by merchant)
facilitator.onBeforeSettle(async (context) => {
  const result = await settleWithRefundHelper(
    context. paymentPayload,
    context.paymentRequirements,
    facilitator.signer
  );

  if (result) {
    return { abort: true, reason: "handled_by_refund_helper" };
  }
  return null;
});

What the helper does:

  1. Extract factoryAddress from extensions.refund. info. factoryAddress
  2. If no extension → return null (not a refund payment)
  3. Check if proxy is deployed (via code check) - if not, throws error (merchant must deploy at registration)
  4. Read merchantPayout from proxy storage
  5. Check merchant is registered with escrow
  6. Route payment through escrow

For Clients

No code changes required.

The payTo address is already swapped to the proxy address, so clients sign normally. The payment automatically routes through escrow.


Optional Helpers

Refund Info Endpoint

Facilitators can optionally provide a /refund-info endpoint that returns refund and arbiter information for a given proxy address or merchant payout. This allows servers and clients to query refund details before or after payment.

Example endpoint response:

{
  "proxyAddress": "0x...",
  "merchantPayout": "0x...",
  "escrowAddress": "0x...",
  "arbiter": "0x...",
  "arbiterUrl": "https://arbiter.example",
  "factoryAddress": "0x...",
  "tokenAddress": "0x..."
}

This information can be used by:

  • Servers: To display refund/arbiter information to users before payment
  • Clients: To query escrow status, dispute information, or arbiter details
  • UIs: To show merchant trust information and dispute resolution options

Comprehensive Verify

For more thorough validation, facilitators can use validateRefundInfo() during the verify phase to check refund extension data before settlement:

import { validateRefundInfo } from "@x402/extensions/refund";

facilitator.onBeforeVerify(async (context) => {
  const { paymentRequirements } = context;

  // Comprehensive validation of refund data
  const isValid = validateRefundInfo(paymentRequirements);

  if (!isValid) {
    return {
      abort: true,
      reason: "invalid_refund_extension"
    };
  }

  // Additional validation can be performed here:
  // - Check proxy is deployed
  // - Verify merchant registration
  // - Validate factory address
  // - Check escrow contract exists

  return null; // Continue with normal verification
});

This provides early validation and better error messages before settlement attempts.


Architecture

Contract Structure

┌─────────────────────────────────────────────────────────────────┐
│  DepositRelayFactory                                            │
│                                                                 │
│  Stores:                                                        │
│  - TOKEN (ERC-20 address)                                       │
│  - IMPLEMENTATION (shared logic)                                │
│  - ESCROW (shared escrow contract)                              │
│  - CREATEX (CREATE3 deployer)                                   │
│                                                                 │
│  Functions:                                                     │
│  - deployRelay(merchantPayout) → deploys proxy via CREATE3      │
│  - computeProxyAddress(merchantPayout) → returns address        │
└─────────────────────────────────────────────────────────────────┘
                              │
                              │ deploys via CREATE3
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  RelayProxy (per merchant)                                      │
│                                                                 │
│  Immutable storage:                                             │
│  - MERCHANT_PAYOUT                                              │
│  - TOKEN                                                        │
│  - ESCROW                                                       │
│  - IMPLEMENTATION                                               │
│                                                                 │
│  Functions:                                                     │
│  - executeDeposit() → delegatecalls to implementation           │
└─────────────────────────────────────────────────────────────────┘
                              │
                              │ delegatecall
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  DepositRelay (shared implementation)                           │
│                                                                 │
│  Logic:                                                         │
│  - Transfer tokens from payer                                   │
│  - Call escrow. noteDeposit()                                   │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  Escrow (shared)                                                │
│                                                                 │
│  Functions:                                                     │
│  - registerMerchant(payout, arbiter)                            │
│  - noteDeposit(merchant, amount, payer)                         │
│  - raiseDispute(depositId, evidence)                            │
│  - resolveDispute(depositId, decision)                          │
│  - release(depositId)                                           │
└─────────────────────────────────────────────────────────────────┘

Security Properties

Property Guarantee
Deterministic addresses Same merchant + factory → same proxy address
Immutable proxy data Cannot be changed after deployment
On-chain verification Merchant registration checked before settlement
Deployment security Merchant must deploy through factory at registration; only factory deployments are officially tracked in the merchant mapping

Extension Schema

The refund extension follows the standard x402 extension format:

{
  "extensions": {
    "refund": {
      "info": {
        "factoryAddress": "0x...",
        "proxies": {
          "0xProxyAddress...": "0xMerchantPayout..."
        }
      },
      "schema": {
        "type": "object",
        "properties": {
          "factoryAddress": { "type": "string" },
          "proxies": {
            "type": "object",
            "additionalProperties": { "type": "string" }
          }
        },
        "required": ["factoryAddress", "proxies"]
      }
    }
  }
}

Our Fee Structure

Zero fees for merchants and clients.

Revenue model:

  • Escrow float is lent in DeFi protocols
  • Yield distributed to arbiters (~75%) and protocol (~25%)

This is the first model. We will support more in the future


API Reference

refundable(paymentOption)

Marks a payment option as refundable.

import { refundable } from "@x402/extensions/refund";

const option = refundable({
  scheme: "exact",
  payTo:  "0xMerchantAddress",
  price:  "$0.01",
  network:  "eip155:84532",
});

Parameters:

  • paymentOption: Standard x402 payment option (must use exact scheme)

Returns: Marked payment option (to be processed by withRefund)


withRefund(routes, factoryAddress, createXAddress? )

Transforms refundable routes by swapping payTo to proxy addresses.

import { withRefund } from "@x402/extensions/refund";

const processed = withRefund(routes, FACTORY_ADDRESS);
// createXAddress is optional - uses standard address for network

Parameters:

  • routes: Route configuration object
  • factoryAddress: X402DepositRelayFactory address
  • createXAddress (optional): CreateX contract address (defaults to standard address)

Returns: Processed routes with:

  • payTo replaced with deterministic proxy address
  • extensions.refund added with factory info

settleWithRefundHelper(paymentPayload, paymentRequirements, signer)

Handles refund payment settlement.

import { settleWithRefundHelper } from "@x402/extensions/refund";

const result = await settleWithRefundHelper(payload, requirements, signer);
if (result) {
  // Refund payment handled - abort normal settlement
}

Returns: null if not a refund payment, or settlement result


Roadmap

Status Feature
✅ Done Escrow and proxy contracts
✅ Done Deterministic address computation
✅ Done Required Extension helpers
✅ Done Merchant registration UI
🚧 In Progress Optional Extension helpers
🚧 In Progress Demo UI
🔮 Future Multi-arbiter support (call center model)
🔮 Future Customer-selectable arbiters out of merchant list
🔮 Future Configurable escrow periods
🔮 Future Multi-token factories

FAQ

Why not a native escrow scheme?

An escrow scheme would require:

  • Client would need to register the new scheme
  • Facilitator would need to register the new scheme
  • Protocol-level changes needed

x402r works with existing infrastructure:

  • Clients need zero changes
  • Facilitators add hooks
  • Uses existing schemes

This enables incremental adoption without breaking changes.

Can anyone deploy the proxy?

The merchant must deploy their proxy at registration time. While anyone could technically deploy a proxy to the CREATE3 address, only deployments through the factory are officially tracked in the factory's merchant mapping, which is required for the system to recognize the proxy as valid. The proxy's immutable storage (merchantPayout, escrow, etc.) is set at deployment based on the factory's configuration.

What if the proxy isn't deployed yet?

The proxy must be deployed by the merchant at registration time before accepting payments. If a payment is attempted to a proxy that hasn't been deployed, the facilitator will reject the payment with an error. The server computes the address without deployment, but the merchant must deploy the proxy before clients can successfully pay to that address.

What if merchant isn't registered?

The facilitator checks registration during settle. If not registered, the payment fails with an error directing to the contracts for registration.

What is CreateX and why is it needed?

CreateX is a smart contract that provides CREATE3 functionality for deterministic address generation. Unlike CREATE2 (which requires bytecode to compute addresses), CREATE3 allows you to compute contract addresses deterministically using only a salt value and the CreateX deployer address.

Why CreateX is needed:

  • No bytecode required: Servers can compute proxy addresses locally without needing the proxy contract's bytecode
  • Deterministic addresses: Same inputs (factory address, merchant payout, CreateX address) always produce the same proxy address
  • Standard implementation: CreateX is a widely-used, audited contract that provides a standard way to deploy contracts via CREATE3 across different networks

The factory uses CreateX as the deployer contract to deploy merchant proxies. The proxy address depends only on the CreateX contract address and the salt (derived from factory address + merchant payout), making address computation simple and efficient.


Resources


This issue is very much open to comments and feedback! We'd also love to help onboard and test with any facilitator or arbiter who'd be interested!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions