Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions specs/schemes/split/scheme_split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Scheme: `split`

## Summary

`split` is a scheme that transfers funds from a client and distributes them to multiple recipients in a single atomic transaction. The split ratios are defined in basis points (BPS) and MUST sum to 10,000 (100%). This enables use cases where payments need to be divided between multiple parties (e.g., content creator + platform, service provider + referrer + protocol).

Unlike the `exact` scheme which transfers to a single `payTo` address, the `split` scheme routes funds through an on-chain splitter contract that atomically distributes to all recipients. This guarantees that either all parties receive their share or the entire payment reverts.

## Use Cases

- **Content platforms**: Reader pays $1.00, author receives 90%, platform receives 10%
- **Referral systems**: Buyer pays for a service, provider gets 70%, referrer gets 20%, protocol gets 10%
- **Agent marketplaces**: Agent pays for a tool, tool creator gets 80%, marketplace gets 15%, protocol gets 5%
- **Sponsorships**: Sponsor pays to support a creator, creator gets majority, platform takes a fee
- **Multi-party services**: Payment is split between multiple service providers who collaborated on a result

## Payment Requirements

The `split` scheme extends the base `PaymentRequirements` with split-specific fields:

```json
{
"scheme": "split",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "1000000",
"maxTimeoutSeconds": 300,
"extra": {
"splitterAddress": "0x...",
"recipients": [
{
"address": "0xAuthor...",
"bps": 9000,
"label": "author"
},
{
"address": "0xProtocol...",
"bps": 1000,
"label": "protocol"
}
]
}
}
```

### Field Definitions

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `splitterAddress` | `string` | MUST | Address of the deployed splitter contract |
| `recipients` | `Recipient[]` | MUST | Array of 2-5 recipients |
| `recipients[].address` | `string` | MUST | Recipient wallet address |
| `recipients[].bps` | `number` | MUST | Basis points (0-10000) for this recipient |
| `recipients[].label` | `string` | MAY | Human-readable label (e.g., "author", "protocol") |

### Validation Rules

- `recipients` array MUST contain between 2 and 5 entries
- Sum of all `recipients[].bps` MUST equal exactly 10,000
- Each `recipients[].bps` MUST be greater than 0
- Each `recipients[].address` MUST be a valid address for the specified network
- `splitterAddress` MUST be a deployed and verified splitter contract
- `amount` represents the total payment; individual shares are computed on-chain

## Splitter Contract Interface

The splitter contract MUST implement the following interface:

```solidity
interface IPaymentSplitter {
struct Recipient {
address addr;
uint256 bps;
}

/// @notice Split a payment to multiple recipients
/// @param token The ERC20 token to split
/// @param totalAmount The total amount to distribute
/// @param recipients Array of recipients with basis point allocations
function split(
address token,
uint256 totalAmount,
Recipient[] calldata recipients
) external;

event PaymentSplit(
address indexed payer,
address indexed token,
uint256 totalAmount,
uint256 recipientCount
);

event RecipientPaid(
address indexed recipient,
address indexed token,
uint256 amount,
uint256 bps
);
}
```

### Contract Behavior

1. Caller MUST have approved the splitter contract to spend `totalAmount` of `token`
2. The contract MUST validate that BPS values sum to 10,000
3. The contract MUST compute each recipient's share: `amount = (totalAmount * bps) / 10000`
4. Remainder from integer division MUST be added to the first recipient's share
5. The contract MUST transfer all shares atomically (all succeed or all revert)
6. The contract MUST emit `PaymentSplit` and `RecipientPaid` events

## Client Flow

1. Client receives 402 with `scheme: "split"` and `extra.recipients`
2. Client approves the `splitterAddress` to spend `amount` of `asset`
3. Client calls `splitter.split(asset, amount, recipients)`
4. Client includes the transaction hash in the payment header

## Verification

Facilitators MUST verify:

1. Transaction exists and is confirmed on the specified network
2. Transaction called the declared `splitterAddress`
3. `PaymentSplit` event was emitted with correct `totalAmount`
4. `RecipientPaid` events match the declared recipients and BPS allocations
5. All recipient addresses match those in `PaymentRequirements.extra.recipients`
6. Total distributed amount equals or exceeds `PaymentRequirements.amount`

## Settlement

Settlement follows the same pattern as `exact`:

1. Facilitator verifies the transaction (see Verification above)
2. If valid, facilitator returns `SettlementResponse` with success
3. Resource server grants access to the protected resource

No additional settlement step is needed because the splitter contract handles distribution atomically during the client's transaction.

## Security Considerations

- **Atomic execution**: All transfers MUST succeed or the entire transaction reverts. Partial distributions MUST NOT occur.
- **BPS validation**: The contract MUST enforce that BPS sum to exactly 10,000 on-chain, regardless of what the server declared. This prevents servers from misconfiguring splits.
- **Rounding**: Integer division remainder MUST be handled deterministically. The spec assigns remainder to the first recipient.
- **Reentrancy**: Splitter contracts MUST use reentrancy guards or follow checks-effects-interactions pattern.
- **Token compatibility**: The contract SHOULD verify the token's `transferFrom` return value or use SafeERC20.
- **No admin keys**: The splitter contract SHOULD be permissionless with no owner or admin functions that could redirect funds.

## Comparison with `exact`

| Feature | `exact` | `split` |
|---------|---------|---------|
| Recipients | 1 | 2-5 |
| On-chain contract | None (direct transfer) | Splitter contract required |
| Atomicity | Single transfer | All-or-nothing multi-transfer |
| Gas cost | ~50k | ~70-120k (varies by recipient count) |
| Use case | Simple payments | Platform fees, referrals, multi-party |

## Appendix

### Reference Implementation

A reference `PaymentSplitter.sol` contract is provided in the x402 repository. Deployments:

- Base Mainnet: (TBD)
- Base Sepolia: (TBD)

### Related Issues

- [#937 - Add new `exact-split` scheme for native facilitator fee support](https://github.com/coinbase/x402/issues/937)
- [#1011 - Escrow Scheme for x402 using Base Commerce Payments Protocol](https://github.com/coinbase/x402/issues/1011)
141 changes: 141 additions & 0 deletions specs/schemes/split/scheme_split_evm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Scheme: `split` `evm`

## Summary

EVM implementation of the `split` scheme for distributing payments to multiple recipients via an on-chain splitter contract. Supports any ERC20 token on EVM-compatible networks (Base, Ethereum, Arbitrum, etc.).

The client approves the splitter contract and calls `split()` with the token, amount, and recipient array. The contract atomically distributes funds using `transferFrom` for each recipient.

## `X-Payment` header payload

The payment payload follows the standard x402 format with scheme-specific data:

```json
{
"x402Version": 2,
"scheme": "split",
"network": "eip155:8453",
"payload": {
"signature": "0x...",
"authorization": {
"from": "0xPayer...",
"to": "0xSplitter...",
"value": "1000000",
"data": "0x...",
"chainId": 8453
}
}
}
```

### Client Construction Steps

1. Parse `PaymentRequirements` from 402 response
2. Extract `splitterAddress` and `recipients` from `extra`
3. Encode `split(address token, uint256 totalAmount, Recipient[] recipients)` calldata
4. Approve splitter to spend `amount` of `asset` (if not already approved)
5. Send transaction calling `splitter.split(asset, amount, recipients)`
6. Include transaction hash in payment header

### Calldata Encoding

```typescript
import { encodeFunctionData } from "viem";

const calldata = encodeFunctionData({
abi: SPLITTER_ABI,
functionName: "split",
args: [
asset, // ERC20 token address
BigInt(amount), // Total amount in smallest units
recipients.map(r => ({ // Recipient array
addr: r.address,
bps: BigInt(r.bps),
})),
],
});
```

## Verification

The facilitator MUST perform the following verification steps:

1. **Transaction receipt**: Fetch receipt for the provided transaction hash. MUST have `status === 1` (success).

2. **Contract target**: The transaction MUST interact with the declared `splitterAddress`. Note: for smart wallets (ERC-4337), the top-level `to` may be the EntryPoint contract. Verify via emitted events instead.

3. **PaymentSplit event**: The `PaymentSplit` event MUST be emitted from `splitterAddress` with:
- `payer` matching the declared client address
- `token` matching `PaymentRequirements.asset`
- `totalAmount >= PaymentRequirements.amount`

4. **RecipientPaid events**: For each recipient in `PaymentRequirements.extra.recipients`, a `RecipientPaid` event MUST be emitted with:
- `recipient` matching the declared address
- `amount` matching `(totalAmount * bps) / 10000` (within rounding tolerance of 1 unit)

5. **Confirmations**: Transaction MUST have at least 1 block confirmation on Base L2.

### Event Topics

```
PaymentSplit: keccak256("PaymentSplit(address,address,uint256,uint256)")
RecipientPaid: keccak256("RecipientPaid(address,address,uint256,uint256)")
```

## Settlement

Settlement is immediate upon verification. The splitter contract executes all transfers atomically during the client's transaction, so no separate settlement step is required.

The facilitator returns:

```json
{
"success": true,
"transaction": "0xTransactionHash...",
"network": "eip155:8453"
}
```

## Appendix

### Gas Costs (Base L2)

| Recipients | Estimated Gas | Approx Cost (Base) |
|------------|--------------|---------------------|
| 2 | ~70,000 | ~$0.001 |
| 3 | ~85,000 | ~$0.0015 |
| 4 | ~100,000 | ~$0.002 |
| 5 | ~120,000 | ~$0.0025 |

### Supported Networks

Any EVM network supported by x402:
- Base Mainnet (`eip155:8453`)
- Base Sepolia (`eip155:84532`)
- Ethereum Mainnet (`eip155:1`)
- Arbitrum (`eip155:42161`)

### Splitter Contract ABI

```json
[
{
"inputs": [
{ "name": "token", "type": "address" },
{ "name": "totalAmount", "type": "uint256" },
{
"name": "recipients",
"type": "tuple[]",
"components": [
{ "name": "addr", "type": "address" },
{ "name": "bps", "type": "uint256" }
]
}
],
"name": "split",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
```