Skip to content

Commit e75f6a9

Browse files
authored
docs: add split payments documentation (#480)
1 parent 7b01e93 commit e75f6a9

7 files changed

Lines changed: 238 additions & 7 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"@stripe/stripe-js": "^8.9.0",
2626
"@vercel/blob": "^2.3.1",
2727
"mermaid": "^11.12.2",
28-
"mppx": "https://pkg.pr.new/mppx@235",
28+
"mppx": "https://pkg.pr.new/mppx@231",
2929
"react": "^19",
3030
"react-dom": "^19",
3131
"stripe": "^20.4.1",

pnpm-lock.yaml

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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>

src/pages/payment-methods/tempo/charge.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,32 @@ const result = await mppx.charge({
7878

7979
When `feePayer` is `true`, the server adds a fee payer signature (domain `0x78`) before broadcasting. The client doesn't need gas tokens. See [fee sponsorship](/payment-methods/tempo#fee-sponsorship) for details.
8080

81+
### With split payments
82+
83+
Split a charge across multiple recipients in a single transaction. The primary `recipient` receives `amount` minus the sum of all splits.
84+
85+
```ts twoslash
86+
import { Mppx, tempo } from "mppx/server";
87+
88+
const mppx = Mppx.create({ methods: [tempo()] });
89+
90+
declare const request: Request;
91+
// ---cut---
92+
const result = await mppx.charge({
93+
amount: "1.00",
94+
currency: "0x20c0000000000000000000000000000000000000", // pathUSD
95+
recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", // seller
96+
splits: [ // [!code hl]
97+
{ // [!code hl]
98+
amount: "0.10", // [!code hl]
99+
recipient: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", // platform fee // [!code hl]
100+
}, // [!code hl]
101+
], // [!code hl]
102+
})(request);
103+
```
104+
105+
Up to 10 splits per charge. Each split must have a positive amount, and the sum of all splits must be less than the total `amount`. See the [split payments guide](/guides/split-payments) for more details.
106+
81107
### With Stripe
82108

83109
```ts twoslash

src/pages/sdk/typescript/client/Method.tempo.charge.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,24 @@ Client identifier used to derive the client fingerprint in attribution memos.
7676

7777
Function that returns a viem client for the given chain ID.
7878

79+
### expectedRecipients (optional)
80+
81+
- **Type:** `readonly string[]`
82+
83+
Allowlist of addresses the client will accept as split recipients. When set, the client rejects any Challenge whose split recipients are not in this list, preventing a compromised server from redirecting funds.
84+
85+
```ts twoslash
86+
import { tempo } from 'mppx/client'
87+
import { privateKeyToAccount } from 'viem/accounts'
88+
89+
const method = tempo.charge({
90+
account: privateKeyToAccount('0xabc…123'),
91+
expectedRecipients: [ // [!code focus]
92+
'0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // [!code focus]
93+
], // [!code focus]
94+
})
95+
```
96+
7997
### mode (optional)
8098

8199
- **Type:** `'push' | 'pull'`

src/pages/sdk/typescript/server/Method.tempo.charge.mdx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,35 @@ Server-defined correlation data, serialized as `opaque` on the Challenge.
176176
- **Type:** `string`
177177

178178
Address to receive the payment.
179+
180+
### splits (optional)
181+
182+
- **Type:** `Array<{ amount: string; memo?: string; recipient: string }>`
183+
184+
Split the charge across additional recipients. Each entry specifies an `amount` (in human-readable units) and a `recipient` address. The primary `recipient` receives `amount` minus the sum of all split amounts.
185+
186+
| Constraint | Value |
187+
|---|---|
188+
| Array length | 1–10 |
189+
| Each split amount | Must be > 0 |
190+
| Sum of splits | Must be strictly less than `amount` |
191+
| Split memo | Optional, 32-byte hex hash |
192+
193+
```ts twoslash
194+
import { Mppx, tempo } from 'mppx/server'
195+
196+
const mppx = Mppx.create({ methods: [tempo.charge()] })
197+
// ---cut---
198+
export async function handler(request: Request) {
199+
const response = await mppx.charge({
200+
amount: '1.00',
201+
currency: '0x20c0000000000000000000000000000000000000', // pathUSD
202+
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // seller
203+
splits: [ // [!code focus]
204+
{ amount: '0.10', recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' }, // platform fee // [!code focus]
205+
], // [!code focus]
206+
})(request)
207+
208+
if (response.status === 402) return response.challenge
209+
return response.withReceipt(Response.json({ data: '...' }))
210+
}

vocs.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ export default defineConfig({
287287
text: "Accept streamed payments",
288288
link: "/guides/streamed-payments",
289289
},
290+
{
291+
text: "Accept split payments",
292+
link: "/guides/split-payments",
293+
},
290294
{
291295
text: "Accept multiple payment methods",
292296
link: "/guides/multiple-payment-methods",

0 commit comments

Comments
 (0)