|
| 1 | +--- |
| 2 | +imageDescription: "Split a single charge across multiple recipients for marketplaces, referral fees, and revenue sharing" |
| 3 | +--- |
| 4 | + |
| 5 | +import { Cards } from 'vocs' |
| 6 | +import { OneTimePaymentsCard, PayAsYouGoCard, ServerQuickstartCard } from '../../components/cards' |
| 7 | + |
| 8 | +# Accept split payments [Distribute a charge across multiple recipients] |
| 9 | + |
| 10 | +Split a single charge across multiple recipients in one atomic transaction. The primary recipient receives the remainder after all splits are deducted. |
| 11 | + |
| 12 | +Split payments are useful for: |
| 13 | + |
| 14 | +- **Marketplaces** — route a platform fee to yourself and the rest to the seller |
| 15 | +- **Referral programs** — pay a bounty to the referrer on every purchase |
| 16 | +- **Revenue sharing** — distribute earnings across partners or contributors |
| 17 | + |
| 18 | +## How it works |
| 19 | + |
| 20 | +When you add `splits` to a charge, the SDK constructs multiple on-chain transfers in a single transaction: |
| 21 | + |
| 22 | +1. Each split recipient receives their declared amount |
| 23 | +2. The primary `recipient` receives `amount - sum(splits)` |
| 24 | +3. The server verifies all transfers atomically |
| 25 | + |
| 26 | +:::info |
| 27 | +Split amounts are in human-readable units, the same as the top-level `amount`. The primary recipient's share is always implicit — you only declare the splits. |
| 28 | +::: |
| 29 | + |
| 30 | +## Server |
| 31 | + |
| 32 | +Add a `splits` array to any `mppx.charge` call. Each entry specifies a `recipient` and `amount`. |
| 33 | + |
| 34 | +```ts twoslash |
| 35 | +import { Mppx, tempo } from 'mppx/server' |
| 36 | + |
| 37 | +const mppx = Mppx.create({ methods: [tempo()] }) |
| 38 | +// ---cut--- |
| 39 | +export async function handler(request: Request) { |
| 40 | + const result = await mppx.charge({ |
| 41 | + amount: '1.00', |
| 42 | + currency: '0x20c0000000000000000000000000000000000000', // pathUSD |
| 43 | + recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // seller |
| 44 | + splits: [ // [!code hl] |
| 45 | + { // [!code hl] |
| 46 | + amount: '0.10', // [!code hl] |
| 47 | + recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // platform fee // [!code hl] |
| 48 | + }, // [!code hl] |
| 49 | + ], // [!code hl] |
| 50 | + })(request) |
| 51 | + |
| 52 | + // seller receives $0.90, platform receives $0.10 |
| 53 | + if (result.status === 402) return result.challenge |
| 54 | + return result.withReceipt(Response.json({ data: '...' })) |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +### With per-split memos |
| 59 | + |
| 60 | +Each split can carry its own on-chain memo for reconciliation: |
| 61 | + |
| 62 | +```ts twoslash |
| 63 | +import { Mppx, tempo } from 'mppx/server' |
| 64 | + |
| 65 | +const mppx = Mppx.create({ methods: [tempo()] }) |
| 66 | + |
| 67 | +declare const request: Request |
| 68 | +// ---cut--- |
| 69 | +const result = await mppx.charge({ |
| 70 | + amount: '1.00', |
| 71 | + currency: '0x20c0000000000000000000000000000000000000', // pathUSD |
| 72 | + memo: '0x6f726465722d313233', // order-123 |
| 73 | + recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // seller |
| 74 | + splits: [ |
| 75 | + { |
| 76 | + amount: '0.10', |
| 77 | + memo: '0x706c6174666f726d2d666565', // platform-fee // [!code hl] |
| 78 | + recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // platform |
| 79 | + }, |
| 80 | + ], |
| 81 | +})(request) |
| 82 | +``` |
| 83 | + |
| 84 | +### With fee sponsorship |
| 85 | + |
| 86 | +Split payments work with [fee sponsorship](/payment-methods/tempo#fee-sponsorship). The server co-signs the multi-transfer transaction so the client doesn't need gas tokens. |
| 87 | + |
| 88 | +```ts twoslash |
| 89 | +import { Mppx, tempo } from 'mppx/server' |
| 90 | + |
| 91 | +const mppx = Mppx.create({ methods: [tempo()] }) |
| 92 | + |
| 93 | +declare const request: Request |
| 94 | +// ---cut--- |
| 95 | +const result = await mppx.charge({ |
| 96 | + amount: '1.00', |
| 97 | + currency: '0x20c0000000000000000000000000000000000000', // pathUSD |
| 98 | + feePayer: true, // [!code hl] |
| 99 | + recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // seller |
| 100 | + splits: [ |
| 101 | + { amount: '0.05', recipient: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC' }, // referrer |
| 102 | + { amount: '0.10', recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' }, // platform |
| 103 | + ], |
| 104 | +})(request) |
| 105 | +``` |
| 106 | + |
| 107 | +## Client |
| 108 | + |
| 109 | +The client SDK handles split payments automatically — no client-side configuration is needed. When the server includes `splits` in the Challenge, the client constructs the matching multi-transfer transaction. |
| 110 | + |
| 111 | +### Validating split recipients |
| 112 | + |
| 113 | +Use `expectedRecipients` to restrict which split recipients the client will sign for. This prevents a compromised server from redirecting funds to unexpected addresses. |
| 114 | + |
| 115 | +```ts twoslash |
| 116 | +import { Mppx, tempo } from 'mppx/client' |
| 117 | +import { privateKeyToAccount } from 'viem/accounts' |
| 118 | + |
| 119 | +const account = privateKeyToAccount('0xabc…123') |
| 120 | +// ---cut--- |
| 121 | +Mppx.create({ |
| 122 | + methods: [ |
| 123 | + tempo.charge({ |
| 124 | + account, |
| 125 | + expectedRecipients: [ // [!code hl] |
| 126 | + '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // platform // [!code hl] |
| 127 | + '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', // referrer // [!code hl] |
| 128 | + ], // [!code hl] |
| 129 | + }), |
| 130 | + ], |
| 131 | +}) |
| 132 | +``` |
| 133 | + |
| 134 | +If the server sends a Challenge with a split recipient not in the allowlist, the client throws an error instead of signing. |
| 135 | + |
| 136 | +## Constraints |
| 137 | + |
| 138 | +| Rule | Limit | |
| 139 | +|------|-------| |
| 140 | +| Splits per charge | 1–10 | |
| 141 | +| Each split amount | Must be > 0 | |
| 142 | +| Sum of all splits | Must be strictly less than `amount` | |
| 143 | +| Split memo | Optional, 32-byte hex hash | |
| 144 | + |
| 145 | +## Next steps |
| 146 | + |
| 147 | +<Cards> |
| 148 | + <OneTimePaymentsCard /> |
| 149 | + <PayAsYouGoCard /> |
| 150 | + <ServerQuickstartCard /> |
| 151 | +</Cards> |
0 commit comments