Skip to content

[Feature Request] Add new exact-split scheme for native facilitator fee support #937

@fretchen

Description

@fretchen

Summary

Propose adding a new exact-split scheme to x402 that enables facilitators to charge fees while allowing sellers to use standard x402 payment requirements without any code changes.

Motivation

Currently, x402 facilitators face a challenge when implementing fee-based business models:

Current Situation Problem
Facilitator wants to charge a fee Seller must adapt their code
Seller specifies payTo: sellerAddress Fee must be handled outside x402
Standard "exact" scheme No built-in fee mechanism

Real-world use case: A public facilitator (no whitelist) wants to charge 0.01 USDC per transaction. Today, this requires:

  1. Seller changes payTo to a splitter contract address
  2. Seller adds extra.seller field with their own address
  3. Seller calculates amount + fee themselves
  4. Both parties must coordinate on the fee structure

This breaks the simplicity of x402 and creates integration friction.

Buyer-Side Complexity

Without native x402 support for fee-based schemes, buyers face significant friction:

  • Must implement a custom SchemeNetworkClient (EIP-712 signing, nonce calculation, payload transformation)
  • Lose the "just use @x402/fetch" simplicity that makes x402 attractive
  • Each fee-based facilitator requires custom client-side code

This creates an adoption blocker for community/small facilitators:

  1. Large, well-funded entities can offer "free" facilitation (subsidized by other business models)
  2. Community facilitators need transaction fees to sustain operations
  3. But buyers won't adopt complex custom schemes → facilitators can't charge fees → only centralized players remain

So from my view native fee support is crucial for a diverse, decentralized facilitator ecosystem. I do not see how I would run one without fees.

Proposed Solution: exact-split Scheme

A new scheme that transforms standard payment requirements internally:

┌─────────────────────────────────────────────────────────────┐
│  Seller's PaymentRequirements (STANDARD!)                   │
│  ─────────────────────────────────────────                  │
│  scheme: "exact-split"                                      │
│  payTo: "0xSeller..."        ← Seller's own address         │
│  amount: "20000"             ← What seller wants (0.02 USDC)│
└─────────────────────────────────────────────────────────────┘
                              ▼
                    ExactSplitEvmScheme
                      transforms to:
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  EIP-712 Authorization (signed by buyer)                    │
│  ───────────────────────────────────────                    │
│  to: "0xSplitter..."         ← Splitter contract            │
│  value: "30000"              ← amount + fee (0.03 USDC)     │
│  nonce: keccak256(seller, salt)                             │
└─────────────────────────────────────────────────────────────┘
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  PaymentPayload.payload                                     │
│  ─────────────────────                                      │
│  authorization: { from, to, value, ... }                    │
│  seller: "0xSeller..."       ← For settlement               │
│  salt: "0x..."               ← Random, for nonce uniqueness │
│  originalAmount: "20000"     ← What seller receives         │
│  fee: "10000"                ← Facilitator fee              │
└─────────────────────────────────────────────────────────────┘

Technical Design

1. SchemeNetworkClient Implementation

class ExactSplitEvmScheme implements SchemeNetworkClient {
    readonly scheme = "exact-split";
    
    async createPaymentPayload(
        x402Version: number,
        requirements: PaymentRequirements
    ): Promise<Pick<PaymentPayload, "x402Version" | "payload">> {
        // 1. Get seller from requirements.payTo
        // 2. Generate random salt
        // 3. Compute nonce = keccak256(abi.encode(seller, salt))
        // 4. Transform: to = splitterAddress, value = amount + fee
        // 5. Sign EIP-712 TransferWithAuthorization
        // 6. Return payload with seller, salt, originalAmount, fee
    }
}

2. Nonce Calculation

// On-chain in Splitter contract:
bytes32 nonce = keccak256(abi.encode(seller, salt));

This ensures:

  • Each seller+salt combination produces a unique nonce
  • Replay protection is maintained
  • Seller address is cryptographically bound to the authorization

3. Splitter Contract Interface

interface IEIP3009Splitter {
    function splitTransferWithAuthorization(
        address from,
        address seller,
        uint256 totalAmount,
        uint256 validAfter,
        uint256 validBefore,
        bytes32 salt,
        bytes calldata signature
    ) external;
}

The contract:

  • Receives totalAmount from buyer
  • Sends totalAmount - fixedFee to seller
  • Sends fixedFee to facilitator wallet
  • All in one atomic transaction

Facilitator /supported Response

{
  "x402Version": 2,
  "kinds": [
    {
      "scheme": "exact-split",
      "network": "eip155:10",
      "asset": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
      "extra": {
        "splitterAddress": "0x...",
        "fixedFee": "10000",
        "feeDescription": "0.01 USDC fixed fee per transaction"
      }
    }
  ]
}

Benefits

Aspect Standard "exact" Proposed "exact-split"
Seller code changes Must adapt for fees None required
Fee transparency Off-chain agreement On-chain, verifiable
Atomic settlement N/A Single transaction
Buyer trust Must trust facilitator Contract-enforced split

Open Questions

  1. Scheme naming: Should it be exact-split, exact-fee, or something else?

  2. Fee discovery: How should the fixedFee be communicated?

    • Option A: In /supported response extra field
    • Option B: Standardized field in PaymentRequirements
    • Option C: Query endpoint on splitter contract
  3. Variable fees: Should the scheme support percentage-based fees, or only fixed fees?

  4. Multi-recipient splits: Should we generalize to N recipients (e.g., royalties)?

  5. Nonce pattern: Is keccak256(seller, salt) the right approach, or should we use a different binding?

On question 3 and 4 I would prefer a "simple" solution for the start.

Reference Implementation

A working PoC is available demonstrating:

  • Custom ExactSplitEvmScheme class implementing SchemeNetworkClient
  • Full verify/settle flow with balance verification

EIP3009SplitterV1 Contract on Optimism Sepolia:

Next Steps

I'm happy to contribute a full implementation (client-side scheme + facilitator logic) as a PR. However, I'd appreciate feedback on the general approach and the open questions above before investing time in a polished PR.

Specifically looking for input on:

  • Is this the right direction for fee support in x402?
  • Any concerns with the nonce binding pattern?
  • Preferences on scheme naming and fee discovery mechanism?

Metadata

Metadata

Assignees

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