Skip to content

Commit f775ef1

Browse files
authored
spec: add split payments to Tempo charge intent (#203)
1 parent b8c42d1 commit f775ef1

File tree

1 file changed

+200
-13
lines changed

1 file changed

+200
-13
lines changed

specs/methods/tempo/draft-tempo-charge-00.md

Lines changed: 200 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ Challenge expiry is conveyed by the `expires` auth-param in
154154
| `methodDetails.chainId` | number | OPTIONAL | Tempo chain ID (default: 42431) |
155155
| `methodDetails.feePayer` | boolean | OPTIONAL | If `true`, server pays transaction fees (default: `false`) |
156156
| `methodDetails.memo` | string | OPTIONAL | A `bytes32` hex value. When present, the client MUST use `transferWithMemo` instead of `transfer`. |
157+
| `methodDetails.splits` | array | OPTIONAL | Additional recipients that receive a portion of `amount`. See {{split-payments}}. |
157158

158159
**Example:**
159160

@@ -172,7 +173,7 @@ Challenge expiry is conveyed by the `expires` auth-param in
172173
The client fulfills this by signing a Tempo Transaction with
173174
`transfer(recipient, amount)` or `transferWithMemo(recipient, amount, memo)`
174175
on the specified `currency` (token address),
175-
with `validBefore` set to the challenge `expires` auth-param. The client SHOULD use a dedicated
176+
with `validBefore` no later than the challenge `expires` auth-param. The client MAY use a dedicated
176177
`nonceKey` (2D nonce lane) for payment transactions to avoid blocking
177178
other account activity if the transaction is not immediately settled.
178179

@@ -181,6 +182,108 @@ If `methodDetails.feePayer` is `true`, the client signs with
181182
server to sponsor fees. If `feePayer` is `false` or omitted, the client
182183
MUST set `fee_token` and pay fees themselves.
183184

185+
## Split Payments {#split-payments}
186+
187+
The `splits` field enables a single charge to distribute payment across
188+
multiple recipients atomically. This is useful for platform fees, revenue
189+
sharing, and marketplace payouts.
190+
191+
### Semantics
192+
193+
The top-level `amount` represents the total amount the client pays. Each
194+
entry in `splits` specifies a recipient and the amount they receive. The
195+
primary recipient (the top-level `recipient`) receives the remainder:
196+
`amount` minus the sum of all split amounts.
197+
198+
### Split Entry Schema
199+
200+
Each entry in the `splits` array is a JSON object:
201+
202+
| Field | Type | Required | Description |
203+
|-------|------|----------|-------------|
204+
| `amount` | string | REQUIRED | Amount in base units for this recipient |
205+
| `memo` | string | OPTIONAL | A `bytes32` hex value for `transferWithMemo` |
206+
| `recipient` | string | REQUIRED | Recipient address |
207+
208+
The `amount` field in each split entry MUST be a base-10 integer string
209+
with no sign, decimal point, exponent, or surrounding whitespace. Each
210+
`splits[i].amount` MUST be greater than zero. The syntax and encoding
211+
requirements for `splits[i].memo` are identical to those for
212+
`methodDetails.memo`, but apply only to that split transfer. Address
213+
fields are compared by decoded 20-byte value, not by string form.
214+
215+
### Constraints
216+
217+
Servers MUST NOT generate a request where the sum of `splits[].amount`
218+
values is greater than or equal to `amount`. Clients MUST reject any
219+
request that violates this constraint. This ensures the primary
220+
recipient always receives a non-zero remainder, avoiding the need to
221+
define zero-value transfer semantics.
222+
223+
Additional constraints:
224+
225+
- If present, `splits` MUST contain at least 1 entry. Servers
226+
SHOULD limit splits to 10 entries to keep gas usage within a
227+
single block's budget (~29,000 gas per additional TIP-20
228+
transfer). Servers MAY reject requests exceeding their supported
229+
split count.
230+
- All transfers MUST target the same `currency` token address.
231+
232+
### Ordering
233+
234+
The order of entries in `splits` is not significant for verification.
235+
Clients SHOULD emit calls in array order. Servers MUST verify that the
236+
required payment effects are present regardless of call ordering.
237+
238+
### Example
239+
240+
~~~json
241+
{
242+
"amount": "1000000",
243+
"currency": "0x20c0000000000000000000000000000000000000",
244+
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
245+
"methodDetails": {
246+
"chainId": 42431,
247+
"feePayer": true,
248+
"splits": [
249+
{
250+
"amount": "50000",
251+
"recipient": "0xA1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
252+
},
253+
{
254+
"amount": "10000",
255+
"memo": "0x00000000000000000000000000000000000000000000000000000000deadbeef",
256+
"recipient": "0xC4D5E6F7A8B9C4D5E6F7A8B9C4D5E6F7A8B9C4D5"
257+
}
258+
]
259+
}
260+
}
261+
~~~
262+
263+
This requests a total payment of 1.00 pathUSD (1,000,000 base units).
264+
The platform receives 0.05 pathUSD, the affiliate receives 0.01 pathUSD
265+
(with a memo), and the primary recipient receives the remaining
266+
0.94 pathUSD (940,000 base units).
267+
268+
### Client Behavior
269+
270+
When `splits` is present, the client MUST produce a transaction whose
271+
on-chain effects include the following `Transfer` or `TransferWithMemo`
272+
events on the `currency` token address:
273+
274+
1. The primary `recipient` receives `amount - sum(splits[].amount)`.
275+
2. Each `splits[i].recipient` receives `splits[i].amount`. If
276+
`splits[i].memo` is present, the corresponding transfer MUST use
277+
`transferWithMemo`.
278+
279+
The top-level `methodDetails.memo`, if present, applies to the primary
280+
transfer.
281+
282+
Clients MAY achieve these effects using any valid transaction structure,
283+
including batched calls, smart contract wallet invocations, or
284+
intermediary operations such as token swaps — provided all required
285+
transfer events are emitted atomically.
286+
184287
# Credential Schema
185288

186289
The credential in the `Authorization` header contains a base64url-encoded
@@ -201,8 +304,11 @@ chain ID applicable to the challenge and the payer's Ethereum address.
201304

202305
When `type` is `"transaction"`, `signature` contains the complete signed
203306
Tempo Transaction (type 0x76) serialized as RLP and hex-encoded with
204-
`0x` prefix. The transaction MUST contain a `transfer(recipient, amount)`
205-
or `transferWithMemo(recipient, amount, memo)` call on the TIP-20 token.
307+
`0x` prefix. The transaction MUST authorize payment in the requested
308+
TIP-20 token sufficient to satisfy the challenge parameters, using one
309+
or more `transfer` and/or `transferWithMemo` calls. When `splits` are
310+
present, the transaction MUST include transfers for each split entry
311+
(see {{split-payments}}).
206312

207313
| Field | Type | Required | Description |
208314
|-------|------|----------|-------------|
@@ -311,8 +417,10 @@ When acting as fee payer, servers:
311417
# Settlement Procedure
312418

313419
For `intent="charge"` fulfilled via transaction, the client signs a
314-
transaction containing a `transfer` or `transferWithMemo` call. If `feePayer: true`, the server
315-
adds its fee payer signature before broadcasting:
420+
transaction containing one or more `transfer` or `transferWithMemo` calls.
421+
When `splits` are present, the transaction contains multiple calls (see
422+
{{split-payments}}). If `feePayer: true`, the server adds its fee payer
423+
signature before broadcasting:
316424

317425
~~~
318426
Client Server Tempo Network
@@ -388,16 +496,33 @@ the transaction. The server verifies the transaction onchain:
388496
Before broadcasting a transaction credential, servers MUST verify:
389497

390498
1. Deserialize the RLP-encoded transaction from `payload.signature`
391-
2. Verify the transaction contains a `transfer(recipient, amount)` or
392-
`transferWithMemo(recipient, amount, memo)` call matching the challenge request
393-
3. Verify the call target matches the `currency` token address
394-
4. Verify the `amount` matches the challenge request amount
395-
5. Verify the `recipient` matches the challenge request recipient
396-
6. If `methodDetails.memo` is present, verify the transaction uses
499+
2. Verify the transaction contains `transfer` or `transferWithMemo`
500+
calls on the `currency` token address
501+
3. Verify the `amount` matches the challenge request amount
502+
4. Verify the `recipient` matches the challenge request recipient
503+
5. If `methodDetails.memo` is present, verify the transaction uses
397504
`transferWithMemo` with the matching memo value
505+
6. If `methodDetails.splits` is present, verify the transaction
506+
includes transfers satisfying each split entry: the primary
507+
recipient receives `amount - sum(splits[].amount)`, each split
508+
recipient receives its specified amount, and any required memo
509+
values are present
510+
511+
Servers MAY impose additional structural requirements (such as
512+
exact call count or ordering) as local policy before broadcasting.
513+
514+
## Hash Verification {#hash-verification}
515+
398516
For hash credentials, servers MUST fetch the transaction receipt and
399-
verify the emitted `Transfer` or `TransferWithMemo` event logs match
400-
the challenge parameters.
517+
verify that it indicates successful execution. Servers MUST verify
518+
that the receipt contains `Transfer` and/or `TransferWithMemo` event
519+
logs emitted by the `currency` token address whose payment effects
520+
satisfy the challenge parameters, including the primary recipient
521+
amount, any split amounts, and any required memo values.
522+
523+
Servers MAY additionally inspect the transaction call data as a
524+
local-policy check, but call-data decoding is not required for
525+
conformance.
401526

402527
## Receipt Generation
403528

@@ -434,6 +559,26 @@ Clients MUST parse and verify the `request` payload before signing:
434559
1. Verify `amount` is reasonable for the service
435560
2. Verify `currency` is the expected token address
436561
3. Verify `recipient` is controlled by the expected party
562+
4. If `splits` is present, verify the sum of split amounts is strictly
563+
less than `amount` and that all split recipients are expected
564+
565+
## Split Payment Risks
566+
567+
When `splits` are present, additional risks apply:
568+
569+
**Recipient Transparency**: Where a human approval step exists, clients
570+
SHOULD present each split recipient and amount so the user can verify
571+
the payment distribution. Clients SHOULD highlight when the primary
572+
recipient receives a small remainder relative to the total `amount`.
573+
574+
**Gas Overhead**: Each additional split adds approximately 29,000 gas
575+
for the TIP-20 precompile transfer execution. A charge with 10 splits
576+
adds approximately 290,000 gas beyond a single-transfer charge. Servers
577+
sponsoring fees via `feePayer: true` MUST budget for the increased gas
578+
limit.
579+
580+
**Split Count Bound**: Servers SHOULD limit `splits` to 10 entries.
581+
See {{split-payments}} for rationale.
437582

438583
## Server-Paid Fees
439584

@@ -526,6 +671,48 @@ Host: api.example.com
526671
Authorization: Payment eyJjaGFsbGVuZ2UiOnsiaWQiOiJrTTl4UHFXdlQybkpySHNZNGFEZkViIn0sInBheWxvYWQiOnsic2lnbmF0dXJlIjoiMHg3NmY5MDEuLi4iLCJ0eXBlIjoidHJhbnNhY3Rpb24ifSwic291cmNlIjoiZGlkOnBraDplaXAxNTU6NDI0MzE6MHgxMjM0NTY3ODkwYWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4In0
527672
~~~
528673

674+
# Split Payment Example
675+
676+
**Challenge with splits:**
677+
678+
~~~http
679+
HTTP/1.1 402 Payment Required
680+
WWW-Authenticate: Payment id="sP1itPaym3ntEx4mple",
681+
realm="marketplace.example.com",
682+
method="tempo",
683+
intent="charge",
684+
request="eyJhbW91bnQiOiIxMDAwMDAwIiwiY3VycmVuY3kiOiIweDIwYzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJtZXRob2REZXRhaWxzIjp7ImNoYWluSWQiOjQyNDMxLCJmZWVQYXllciI6dHJ1ZSwic3BsaXRzIjpbeyJhbW91bnQiOiI1MDAwMCIsInJlY2lwaWVudCI6IjB4QTFCMkMzRDRFNUY2QTFCMkMzRDRFNUY2QTFCMkMzRDRFNUY2QTFCMiJ9XX0sInJlY2lwaWVudCI6IjB4NzQyZDM1Q2M2NjM0QzA1MzI5MjVhM2I4NDRCYzllNzU5NWY4ZkUwMCJ9",
685+
expires="2025-06-01T12:00:00Z"
686+
Cache-Control: no-store
687+
~~~
688+
689+
The `request` decodes to:
690+
691+
~~~json
692+
{
693+
"amount": "1000000",
694+
"currency": "0x20c0000000000000000000000000000000000000",
695+
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
696+
"methodDetails": {
697+
"chainId": 42431,
698+
"feePayer": true,
699+
"splits": [
700+
{
701+
"amount": "50000",
702+
"recipient": "0xA1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
703+
}
704+
]
705+
}
706+
}
707+
~~~
708+
709+
This requests a total payment of 1.00 pathUSD. The platform receives
710+
0.05 pathUSD and the merchant receives 0.95 pathUSD. The resulting
711+
transaction must emit the following transfer events:
712+
713+
1. 950,000 to `0x742d...fE00` — merchant receives remainder
714+
2. 50,000 to `0xA1B2...A1B2` — platform fee
715+
529716
# Acknowledgements
530717

531718
The authors thank the Tempo community for their feedback on this

0 commit comments

Comments
 (0)