@tollstile/mpp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paradigm AI Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # @tollstile/mpp
2
+
3
+ [Machine Payments Protocol](https://paymentauth.org) rails for [Tollstile](https://tollstile.com): `WWW-Authenticate: Payment` challenges and credentials over HTTP and MCP, with three payment methods.
4
+
5
+ | Rail | MPP method / intent | Status |
6
+ |---|---|---|
7
+ | `mppStripe()` | `stripe` / `charge` (Shared Payment Tokens) | Stable wire, fakes only |
8
+ | `mppTempo()` | `tempo` / `charge` (TIP-20 transfer, pull and push) | Stable wire, fakes only |
9
+ | `mppTempoSession()` | `tempo` / `session` v2 (payment channels, `voucher` action) | **Experimental** |
10
+
11
+ ```bash
12
+ npm install tollstile @tollstile/mpp
13
+ ```
14
+
15
+ ```ts
16
+ import { createTollstile } from "tollstile";
17
+ import { mppStripe, mppTempo } from "@tollstile/mpp";
18
+ import { tollstile } from "@tollstile/hono";
19
+
20
+ const toll = createTollstile({
21
+ rails: [
22
+ mppStripe({
23
+ realm: "api.example.com",
24
+ secret: process.env.MPP_SECRET!, // binds challenge ids; ≥ 32 chars, list to rotate
25
+ secretKey: process.env.STRIPE_SECRET_KEY!,
26
+ networkId: "profile_1MqDcVKA5fEO2tZvKQm9g8Yj",
27
+ }),
28
+ mppTempo({
29
+ realm: "api.example.com",
30
+ secret: process.env.MPP_SECRET!,
31
+ rpcUrl: "https://rpc.moderato.tempo.xyz",
32
+ chainId: 42431,
33
+ recipient: "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
34
+ token: { address: "0x20c0000000000000000000000000000000000000", code: "pathUSD" },
35
+ denomination: "USD",
36
+ }),
37
+ ],
38
+ ledger, // e.g. postgresLedger(db)
39
+ secret: process.env.TOLLSTILE_SECRET!,
40
+ });
41
+
42
+ app.get("/report", tollstile(toll.price("$1.00")), (c) => c.json({ ok: true }));
43
+ ```
44
+
45
+ ## Wire format (shared by every rail)
46
+
47
+ - **Challenge**: one `WWW-Authenticate: Payment id, realm, method, intent, request, expires, opaque` per rail. `request` and `opaque` are base64url (no padding) of RFC 8785 JCS JSON. `expires` is always set (the quote's expiry, rounded down to the second). `header` and `description` are never issued, so the credential is always read from `Authorization`.
48
+ - **Binding**: `id = base64url(HMAC-SHA256(secret, realm|method|intent|request|expires|digest|opaque))` via `crypto.subtle`, matching the spec's recommended slots and mppx's published vectors. The first secret signs; every secret in the list verifies (rotation). Ids are compared in constant time against all secrets.
49
+ - **Quote carriage**: Tollstile's signed quote token travels in `opaque` as `{"tollstile_quote": …}`. It is bound by the HMAC and opened with `terms.openQuote`, so the quoted price is what is charged.
50
+ - **Verification** of a credential: HMAC id → realm → expiry → quote → the echoed `request` must be byte-identical (JCS) to what this server issues for that quote → method-specific proof. Credentials for another `method`/`intent` are `absent`, so several MPP rails share one request.
51
+ - **Receipts**: `Payment-Receipt` (base64url JCS JSON, with `challengeId`) plus `Cache-Control: private` on HTTP; `_meta["org.paymentauth/receipt"]` on MCP.
52
+ - **MCP**: credentials from `_meta["org.paymentauth/credential"]` (native `request` JSON is accepted); `challenge.mcp` is `{ style: "mpp", challenge }` for the adapter's `-32042` error.
53
+ - **proofId** is the challenge id (single-use enforced by the ledger), except for sessions, where it is the channel id.
54
+ - **Idempotency key**: the charge rails return the challenge id as `idempotencyKey` (a client `Idempotency-Key` takes precedence). A challenge is issued for one 402 and paid once, so every presentation of its credential is the same logical request: a retry after success answers `409 already_paid` with the settlement reference, a retry while the outcome is unknown answers `503 payment_outcome_unknown`, and a retry after a release runs again. For Stripe it is also the key the PaymentIntent is deduplicated on. Sessions return none: one channel pays many requests, so only the client can say which requests are retries.
55
+ - **Rejected but already paid**: when a credential this server issued can no longer be accepted (its challenge or quote expired, or a Tempo pull transaction is past `validBefore`), the charge rails still return its `proofId`, so a retry of a request that was paid is answered from the ledger (`409`) instead of a `402` asking the client to pay again. Sessions do not, since their proof id is the channel.
56
+ - **Payers** are canonical: `did:pkh:eip155:<chainId>:<lowercase address>` for Tempo charge and session, `stripe:<challengeId>` for Stripe.
57
+
58
+ ## `mppStripe(options)`
59
+
60
+ | Option | Default | Purpose |
61
+ |---|---|---|
62
+ | `realm`, `secret` | required | Challenge realm and HMAC secret(s) |
63
+ | `secretKey` | required | Stripe API key, sent only as `Authorization: Bearer` |
64
+ | `networkId` | required | Stripe Business Network Profile id (`methodDetails.networkId`) |
65
+ | `paymentMethodTypes` | `["card"]` | `methodDetails.paymentMethodTypes` |
66
+ | `apiVersion` | `"2026-07-29.preview"` | `Stripe-Version`. SPTs require a preview version (mppx 0.9.3 uses this one) |
67
+ | `sptParameter` | `"shared_payment_granted_token"` | The MPP spec / mppx name. Stripe's SPT guide shows `payment_method_data[shared_payment_granted_token]`; switch if your account needs it |
68
+ | `searchLagMs` | 10 minutes | How long a Stripe Search miss is not trusted after an ambiguous settlement |
69
+ | `apiBase`, `fetch`, `clock` | Stripe, global, system | Injection points |
70
+
71
+ | Capability | Value | Why |
72
+ |---|---|---|
73
+ | `flows` | `['upfront']` | Confirming a PaymentIntent captures immediately; there is no hold to release |
74
+ | `authorization` | `single` | One SPT, one payment |
75
+ | `refund` / `partialRefund` | `true` / `true` | Stripe Refunds API, idempotency key = `operation.key` |
76
+ | `variableAmount` | `false` | The SPT is granted for the challenged amount |
77
+ | `quotes` | `true` | Carried in `opaque` |
78
+ | `lookup` | `true` | See below |
79
+
80
+ - **Offers**: `null` for currencies without a known minor unit, amounts finer than the minor unit (sub-cent USD), and amounts below Stripe's general minimum (USD $0.50, GBP £0.30, …). A route priced below the minimum with `mppStripe` as its only rail answers `402` with an empty `accepts`; add a rail that can serve small amounts.
81
+ - **Idempotency key: `tollstile_mpp_<challengeId>`**, not the spec's `${challenge.id}_${spt}` and not `operation.key`. A single challenge may be presented again after its charge was released (the core retry path). A per-charge key would let that retry create a second PaymentIntent; a key containing the SPT would do the same if the payer retried with a new SPT. One key per challenge means Stripe replays the first PaymentIntent (or answers an idempotency conflict, which is treated as ambiguous) instead of charging twice. Challenges expire in minutes, well inside Stripe's 24-hour key retention. Parameters are identical across retries (metadata holds only `challenge_id` and `tollstile_authorization`), so replays succeed. A replayed PaymentIntent is judged by its status.
82
+ - **The SPT never reaches the ledger.** It is a bearer token, so it stays in process memory, keyed by request, until its challenge expires (so a repeated `settle` replays the PaymentIntent through Stripe's idempotency). Another process cannot settle with it, and does not need to: the upfront flow never re-settles from reconciliation, it looks up.
83
+ - **Lookup without creating a charge**: retrieve by PaymentIntent id when the charge has one; otherwise Stripe Search `metadata['challenge_id']:'<id>'`, re-checking the metadata of every hit. `processing`/`requires_capture` stay unknown. Refunds are found by `metadata.tollstile_charge`.
84
+ - **Eventual-consistency risk**: Stripe Search is typically current within a minute but can lag longer during incidents. A miss younger than `searchLagMs` (measured from the charge's last transition) stays unknown. If Search lags longer than that, reconciliation releases the charge while a PaymentIntent exists: the payer is charged and the ledger says released. A retry of the same credential still replays that PaymentIntent (no second charge), but without a retry it is only visible in Stripe. Keep `searchLagMs` generous and reconcile Stripe payouts against the ledger.
85
+
86
+ ## `mppTempo(options)`
87
+
88
+ | Option | Default | Purpose |
89
+ |---|---|---|
90
+ | `realm`, `secret` | required | Challenge realm and HMAC secret(s) |
91
+ | `rpcUrl`, `chainId` | required | Tempo JSON-RPC (`4217` mainnet, `42431` Moderato) |
92
+ | `recipient` | required | Payee address |
93
+ | `token` | required | TIP-20 `{ address, code }` (6 decimals) |
94
+ | `denomination` | required | Price currency the token is worth at par, e.g. `"USD"`. Other currencies get no offer |
95
+ | `modes` | `["pull"]` | `"push"` is opt-in (see below) |
96
+ | `splits` | none | `(amount) => [{ recipient, amount, memo? }]` in base units; sum must stay below the total |
97
+ | `validityMarginMs` | 60 s | Block-timestamp skew allowed past a transaction's `validBefore` |
98
+ | `fetch`, `clock` | global, system | Injection points |
99
+
100
+ | Capability | Value | Why |
101
+ |---|---|---|
102
+ | `flows` | `['authorization']` | A pull transaction can be broadcast any time before `validBefore`, so it is broadcast only after the handler succeeds; a failed handler costs the payer nothing |
103
+ | `authorization` | `single` | One transfer |
104
+ | `refund` | `false` | Refunding would require the rail to sign transfers with a merchant key |
105
+ | `variableAmount` | `false` | The signed amount is fixed |
106
+ | `quotes` | `true` | Carried in `opaque` |
107
+ | `lookup` | `true` | `eth_getTransactionReceipt` by the transaction hash |
108
+
109
+ - **Challenge binding on-chain**: every request carries `methodDetails.memo = keccak256("tollstile/mpp:<realm>:<quote id>:<quote nonce>")`, and the primary transfer must be `transferWithMemo` with it. A transfer made for one challenge cannot satisfy another, so one on-chain payment cannot be presented twice under two challenge ids.
110
+ - **Pull verification (offline)**: strict RLP decode of the `0x76` envelope, secp256k1 sender recovery (low-s), chain id, `validBefore` present, in the future and not after the challenge `expires`, `validAfter` not in the future, and the calls must be exactly the required transfers on the token (primary with memo, plus splits). Refused as local policy: fee-payer sponsorship (`feePayer` is never offered), key authorizations, authorization lists, non-secp256k1 signatures, and extra calls.
111
+ - **Signed transaction in the ledger**: stored in authorization data so settlement survives a crash, and dropped by `redact` when a charge becomes final (the hash and `validBefore` stay for lookup). A released charge keeps it, so the same credential can be retried. Settling again after redaction answers from the transaction receipt.
112
+ - **Settlement**: `eth_sendRawTransactionSync`. Rebroadcasting the same bytes cannot transfer twice (one nonce). A lost answer or a refusal without a receipt stays unknown until the transaction can no longer be included (`validBefore` + margin); only then is it rejected.
113
+ - **Residual risk (authorization flow)**: between verification and broadcast the payer can spend the nonce or the balance. The handler has then run unpaid; the charge ends `failed/completed` and `onEvent` reports `SETTLEMENT_REJECTED`. The window is the handler's duration. Balance simulation before admission is not implemented.
114
+ - **Push mode** (`modes: ["pull", "push"]`): the payer broadcasts and sends the hash; the receipt's `Transfer`/`TransferWithMemo` logs are checked at verification, and `verify` returns `settled` with the transaction hash. Core records the charge with flow `upfront` (money moved before the handler, even though the rail declares only `authorization` for pull mode) as `settled/running` before the handler. Because this rail cannot refund, a failed handler leaves the charge `settled/failed` with a `REFUND_REJECTED` event, and reconciliation skips it (`RECONCILIATION_SKIPPED`). Presenting the credential again answers `409 already_paid`. Enable push only if you are willing to keep payments for failed handlers and handle them yourself.
115
+
116
+ ## `mppTempoSession(options)` — experimental
117
+
118
+ Options: `realm`, `secret`, `rpcUrl`, `chainId`, `recipient` (payee), `token`, `denomination`, `escrow` (default TIP-20 channel precompile `0x4d50…0000`), `operator` (default none), `fetch`, `clock`.
119
+
120
+ | Capability | Value | Why |
121
+ |---|---|---|
122
+ | `flows` | `['upfront']` | Voucher coverage can only be checked in `settle`, where the authorization's consumption is known; settling before the handler means an uncovered call is refused before it runs |
123
+ | `authorization` | `reusable` | The authorization is the channel (`proofId` = channel id) |
124
+ | `limit` | deposit − on-chain `settled`, at first sight | Ledger capacity |
125
+ | `refund` | `true` | Nothing is captured per charge; a refund removes the charge from consumption, and the close helper captures consumption only |
126
+ | `partialRefund`, `variableAmount` | `false` | Not implemented |
127
+ | `lookup` | `true` | Settle and refund have no external effect, so lookup is exact: interrupted refunds are complete, interrupted settlements never happened |
128
+
129
+ - **Verification** (`action: "voucher"`): descriptor payee/token/operator, recomputed v2 channel id, EIP-712 `Voucher(bytes32 channelId,uint96 cumulativeAmount)` under the `TIP20 Channel Reserve` domain recovered to `authorizedSigner` or payer (low-s), and live channel state via `getChannelState`: exists, no close requested, voucher ≤ deposit and ≥ settled.
130
+ - **Settlement of a charge** accepts the voucher only if `cumulativeAmount ≥ baseline + consumed + reserved` of the authorization, where `reserved` already includes this charge and every other in-flight charge. Concurrent calls therefore can never be covered by the same voucher value; replaying a voucher is harmless. The voucher is recorded in the charge's `settlement.details`.
131
+ - **What `settled` means here**: the payee holds a payer-signed voucher covering the charge. It does **not** mean funds moved. Funds move when you close the channel on-chain with `tempoSessionClose({ authorization, charges, settledOnChain? })`, which returns `{ to, data, captureAmount, cumulativeAmount }` for `close(descriptor, cumulativeAmount, captureAmount, signature)`: it captures `max(baseline + ledger consumption, settledOnChain)` using the highest voucher, and refunds the rest of the deposit to the payer. Submit it from the payee account with your own wallet; the package holds no keys.
132
+ - **Guarantee gap**: a payer can `requestClose()` and `withdraw()` after the escrow's grace period (15 minutes in the reference contract). Anything not captured by then is lost to the merchant, even though the ledger says `settled`. Watch for `CloseRequested` and close promptly. Never call the escrow's `settle()` with the highest voucher directly: it captures the full voucher, including refunded or unused value.
133
+ - **Not supported**: `open`, `topUp`, and `close` credentials (the payer must open and fund the channel on-chain before sending vouchers), session protocol v1, top-ups raising the ledger limit, and SSE/WebSocket metering.
134
+ - **Why experimental**: vouchers pass from `verify` to `settle` in process memory (a rail cannot write to the authorization after it is opened), and the rail cannot see consumption at verification. Both are safe in the upfront flow as implemented, but they rule out the authorization flow and variable prices. The core change that removes this: pass the stored authorization (or a `LedgerReader`) into `verify` for rails with `authorization: 'reusable'`, and let `openAuthorization` carry a per-charge proof payload onto `NewCharge` (e.g. `NewCharge.proof: Json`, handed to `settle` as `charge.proof`).
135
+
136
+ ## Core change needed
137
+
138
+ - **Sessions** (deferred past v0.1): `NewCharge.proof` (per-request proof data persisted with the charge and passed to `settle`) and read access to the existing authorization in `verify`, as described above.
139
+
140
+ ## Verification status
141
+
142
+ Everything here was tested **only against in-process fakes and published vectors**, never against Stripe or a Tempo node.
143
+
144
+ **Rail conformance** (`railConformance()` from `tollstile/testing`, `test/conformance.test.ts`):
145
+
146
+ | Rail | Result |
147
+ |---|---|
148
+ | `mppStripe` | all cases pass; redaction skipped (no evidence is stored) |
149
+ | `mppTempo` pull | all cases pass |
150
+ | `mppTempo` push | all run cases pass (a failed handler leaves the charge `settled/failed`). The settle-fault and tamper cases are skipped: the rail never settles push payments, and a tampered push proof is itself an on-chain transfer the kit would count as a settlement |
151
+ | `mppTempoSession` | not run: experimental, reusable authorization whose settlement is off-chain until close |
152
+
153
+ - Challenge ids: mppx 0.9.3's HMAC test vectors (`test-vector-secret`), JCS: RFC 8785 examples.
154
+ - Stripe: an in-memory Stripe with idempotency replay/conflict, Search visibility lag, refunds, declines, 5xx, and dropped connections.
155
+ - Tempo: transactions built and signed in the tests with `@noble/curves` keys and the package's own RLP encoder, a fake JSON-RPC node for `eth_sendRawTransactionSync`, `eth_getTransactionReceipt`, and `eth_call`. The `0x76` field layout and sign hash follow `ox` `TxEnvelopeTempo`; EIP-712 voucher hashing follows `ox` `Channel.getVoucherSignPayload`. No bytes from a real Tempo client were used.
156
+
157
+ To verify live:
158
+
159
+ 1. **Stripe (test mode)**: create an SPT with `POST /v1/test_helpers/shared_payment/granted_tokens` (`payment_method=pm_card_visa`, usage limits for the challenged amount, preview `Stripe-Version`), send it as the credential for a $0.50+ route, and confirm a `succeeded` PaymentIntent with `metadata.challenge_id`. Check which `sptParameter` your account accepts. Force a handler failure and confirm the refund. Then settle once with a blocked network and run `reconcile()` after `searchLagMs` to confirm Search finds the PaymentIntent. Run `npx mppx@latest validate <url>` against the endpoint.
160
+ 2. **Tempo charge (Moderato, chain 42431)**: pay a challenge with the `mppx` client in pull mode and confirm the transaction hash in the receipt on the explorer; check that the client uses the challenge `memo` with `transferWithMemo` and does not request fee sponsorship. Repeat with push mode if enabled. Compare `decodeTempoTransaction` against a transaction serialized by `viem/tempo`.
161
+ 3. **Tempo session**: open a v2 channel on Moderato with the `mppx` session client, send `voucher` credentials to a priced route, then submit `tempoSessionClose()` calldata from the payee and confirm the capture amount and payer refund on-chain.
162
+
163
+ MIT © 2026 Paradigm AI Inc.
@@ -0,0 +1,249 @@
1
+ import { Clock, Rail, Authorization, Charge } from 'tollstile';
2
+
3
+ type MppStripeOptions = {
4
+ /** The protection space advertised in challenges, e.g. `"api.example.com"`. ASCII. */
5
+ readonly realm: string;
6
+ /** Binds challenge ids (HMAC-SHA256). At least 32 characters. Pass a list to rotate: the first signs, all verify. */
7
+ readonly secret: string | readonly string[];
8
+ /** Stripe secret key (`sk_live_…` or `sk_test_…`). Never leaves the Authorization header. */
9
+ readonly secretKey: string;
10
+ /** Your Stripe Business Network Profile id (`profile_…`), advertised as `methodDetails.networkId`. */
11
+ readonly networkId: string;
12
+ /** Payment method types you can process. Defaults to `["card"]`. */
13
+ readonly paymentMethodTypes?: readonly string[];
14
+ /** `Stripe-Version` sent with every call. SPTs need a preview version. Defaults to `"2026-07-29.preview"`. */
15
+ readonly apiVersion?: string;
16
+ /**
17
+ * The PaymentIntent parameter carrying the SPT. The MPP spec and mppx use
18
+ * `shared_payment_granted_token`; Stripe's SPT guide shows
19
+ * `payment_method_data[shared_payment_granted_token]`. Defaults to the spec's form.
20
+ */
21
+ readonly sptParameter?: 'shared_payment_granted_token' | 'payment_method_data[shared_payment_granted_token]';
22
+ /**
23
+ * How long after an ambiguous settlement a Stripe Search miss is trusted as "no payment".
24
+ * Search is eventually consistent (usually under a minute). Defaults to 10 minutes.
25
+ */
26
+ readonly searchLagMs?: number;
27
+ /** Defaults to `https://api.stripe.com`. */
28
+ readonly apiBase?: string;
29
+ readonly fetch?: typeof fetch;
30
+ readonly clock?: Clock;
31
+ };
32
+ /** What `settle`, `refund`, and `lookup` need in any process. The SPT itself is never stored. */
33
+ type MppStripeData = {
34
+ readonly challengeId: string;
35
+ /** In the currency's minor unit. */
36
+ readonly amount: string;
37
+ readonly currency: string;
38
+ };
39
+ type MppStripeRail = Rail<'mpp-stripe', MppStripeData>;
40
+ /**
41
+ * MPP `stripe` `charge`: the payer sends a Shared Payment Token and the rail confirms a
42
+ * PaymentIntent with it. Stripe captures synchronously, so this rail settles before the handler
43
+ * (`upfront`) and refunds when the handler fails.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const toll = createTollstile({
48
+ * rails: [
49
+ * mppStripe({
50
+ * realm: 'api.example.com',
51
+ * secret: process.env.MPP_SECRET,
52
+ * secretKey: process.env.STRIPE_SECRET_KEY,
53
+ * networkId: 'profile_1MqDcVKA5fEO2tZvKQm9g8Yj',
54
+ * }),
55
+ * ],
56
+ * ledger,
57
+ * secret: process.env.TOLLSTILE_SECRET,
58
+ * });
59
+ * app.get('/report', tollstile(toll.price('$1.00')), handler);
60
+ * ```
61
+ */
62
+ declare function mppStripe(options: MppStripeOptions): MppStripeRail;
63
+
64
+ type Address = `0x${string}`;
65
+
66
+ type TempoMode = 'pull' | 'push';
67
+ /** A TIP-20 token. TIP-20 tokens have 6 decimals. */
68
+ type TempoToken = {
69
+ /** Token address, e.g. pathUSD `0x20c0000000000000000000000000000000000000`. */
70
+ readonly address: string;
71
+ /** Asset code recorded on offers, e.g. `"pathUSD"`. */
72
+ readonly code: string;
73
+ };
74
+ type TempoSplit = {
75
+ readonly recipient: string;
76
+ /** In token base units. */
77
+ readonly amount: bigint;
78
+ readonly memo?: string;
79
+ };
80
+ type MppTempoOptions = {
81
+ /** The protection space advertised in challenges, e.g. `"api.example.com"`. ASCII. */
82
+ readonly realm: string;
83
+ /** Binds challenge ids (HMAC-SHA256). At least 32 characters. Pass a list to rotate: the first signs, all verify. */
84
+ readonly secret: string | readonly string[];
85
+ /** JSON-RPC endpoint, e.g. `https://rpc.tempo.xyz` or Moderato testnet `https://rpc.moderato.tempo.xyz`. */
86
+ readonly rpcUrl: string;
87
+ /** `4217` for mainnet, `42431` for Moderato testnet. Must match `rpcUrl`. */
88
+ readonly chainId: number;
89
+ /** The address that receives payments. */
90
+ readonly recipient: string;
91
+ readonly token: TempoToken;
92
+ /**
93
+ * The price currency the token is worth at par, e.g. `"USD"` for a USD stablecoin. Prices in
94
+ * any other currency get no offer: Tollstile never converts.
95
+ */
96
+ readonly denomination: string;
97
+ /**
98
+ * `pull` (default): the payer signs a transaction and the rail broadcasts it after the handler.
99
+ * `push`: the payer broadcasts first and sends the hash. Push payments have already moved when
100
+ * the handler runs, and this rail cannot refund them; see the README before enabling it.
101
+ */
102
+ readonly modes?: readonly TempoMode[];
103
+ /** Additional recipients per charge, computed from the total in base units. Their sum must stay below the total. */
104
+ readonly splits?: (amount: bigint) => readonly TempoSplit[];
105
+ /**
106
+ * How long after a signed transaction's `validBefore` a missing receipt is trusted as final,
107
+ * covering block-timestamp skew. Defaults to 60 seconds.
108
+ */
109
+ readonly validityMarginMs?: number;
110
+ readonly fetch?: typeof fetch;
111
+ readonly clock?: Clock;
112
+ };
113
+ type MppTempoData = {
114
+ readonly challengeId: string;
115
+ readonly mode: TempoMode;
116
+ readonly hash: string;
117
+ /**
118
+ * The signed transaction to broadcast (pull), kept so settlement survives a crash. `null` for push,
119
+ * and dropped by `redact` once a charge on the authorization is final.
120
+ */
121
+ readonly transaction: string | null;
122
+ /** Unix seconds (pull). */
123
+ readonly validBefore: string | null;
124
+ };
125
+ type MppTempoRail = Rail<'mpp-tempo', MppTempoData>;
126
+ /**
127
+ * MPP `tempo` `charge`: a one-time TIP-20 transfer on Tempo. In pull mode the payer signs a
128
+ * transaction that this rail verifies offline and broadcasts only after the handler succeeds, so a
129
+ * failed handler costs the payer nothing.
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * const toll = createTollstile({
134
+ * rails: [
135
+ * mppTempo({
136
+ * realm: 'api.example.com',
137
+ * secret: process.env.MPP_SECRET,
138
+ * rpcUrl: 'https://rpc.moderato.tempo.xyz',
139
+ * chainId: 42431,
140
+ * recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
141
+ * token: { address: '0x20c0000000000000000000000000000000000000', code: 'pathUSD' },
142
+ * denomination: 'USD',
143
+ * }),
144
+ * ],
145
+ * ledger,
146
+ * secret: process.env.TOLLSTILE_SECRET,
147
+ * });
148
+ * ```
149
+ */
150
+ declare function mppTempo(options: MppTempoOptions): MppTempoRail;
151
+
152
+ /** The immutable identity of a v2 channel. Every field is bound into the channel id. */
153
+ type ChannelDescriptor = {
154
+ readonly payer: Address;
155
+ readonly payee: Address;
156
+ readonly operator: Address;
157
+ readonly token: Address;
158
+ readonly salt: `0x${string}`;
159
+ readonly authorizedSigner: Address;
160
+ readonly expiringNonceHash: `0x${string}`;
161
+ };
162
+
163
+ type MppTempoSessionOptions = {
164
+ /** The protection space advertised in challenges, e.g. `"api.example.com"`. ASCII. */
165
+ readonly realm: string;
166
+ /** Binds challenge ids (HMAC-SHA256). At least 32 characters. Pass a list to rotate: the first signs, all verify. */
167
+ readonly secret: string | readonly string[];
168
+ readonly rpcUrl: string;
169
+ /** `4217` for mainnet, `42431` for Moderato testnet. */
170
+ readonly chainId: number;
171
+ /** The channel payee: the address that can close channels and receives captured funds. */
172
+ readonly recipient: string;
173
+ readonly token: TempoToken;
174
+ /** The price currency the token is worth at par, e.g. `"USD"`. */
175
+ readonly denomination: string;
176
+ /** Defaults to the TIP-20 channel escrow precompile `0x4d50500000000000000000000000000000000000`. */
177
+ readonly escrow?: string;
178
+ /** Payee-side operator bound into channel descriptors. Defaults to none (the zero address). */
179
+ readonly operator?: string;
180
+ readonly fetch?: typeof fetch;
181
+ readonly clock?: Clock;
182
+ };
183
+ /** Channel identity recorded on the authorization. `baseline` is the on-chain `settled` when Tollstile first saw the channel. */
184
+ type MppTempoSessionData = {
185
+ readonly channelId: string;
186
+ readonly descriptor: {
187
+ readonly [K in keyof ChannelDescriptor]: string;
188
+ };
189
+ readonly baseline: string;
190
+ };
191
+ type MppTempoSessionRail = Rail<'mpp-tempo-session', MppTempoSessionData>;
192
+ /**
193
+ * **Experimental.** MPP `tempo` `session` (protocol v2) on Tempo payment channels. The channel is a
194
+ * reusable authorization whose limit is its deposit; each paid call is a charge that settles by
195
+ * holding a payer-signed cumulative voucher covering everything consumed on the channel.
196
+ *
197
+ * Settlement here is off-chain. Funds reach the payee only when the channel is closed on-chain
198
+ * with {@link tempoSessionClose}, which must happen before a payer's forced close completes.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const session = mppTempoSession({
203
+ * realm: 'api.example.com',
204
+ * secret: process.env.MPP_SECRET,
205
+ * rpcUrl: 'https://rpc.moderato.tempo.xyz',
206
+ * chainId: 42431,
207
+ * recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
208
+ * token: { address: '0x20c0000000000000000000000000000000000000', code: 'pathUSD' },
209
+ * denomination: 'USD',
210
+ * });
211
+ * const toll = createTollstile({ rails: [session], ledger, secret: process.env.TOLLSTILE_SECRET });
212
+ * ```
213
+ */
214
+ declare function mppTempoSession(options: MppTempoSessionOptions): MppTempoSessionRail;
215
+ type TempoSessionCloseInput = {
216
+ /** The channel's authorization, from your ledger. */
217
+ readonly authorization: Authorization;
218
+ /** Settled charges on that authorization; the highest voucher among them is used. */
219
+ readonly charges: readonly Charge[];
220
+ /** The channel's current on-chain `settled`, if you read it. Capture never goes below it. */
221
+ readonly settledOnChain?: bigint;
222
+ /** Defaults to the TIP-20 channel escrow precompile. */
223
+ readonly escrow?: string;
224
+ };
225
+ type TempoSessionClose = {
226
+ readonly channelId: string;
227
+ /** The escrow address to call. */
228
+ readonly to: Address;
229
+ /** `close(descriptor, cumulativeAmount, captureAmount, signature)` calldata. */
230
+ readonly data: `0x${string}`;
231
+ /** Base units the payee receives in total: baseline plus what the ledger records as consumed. */
232
+ readonly captureAmount: bigint;
233
+ readonly cumulativeAmount: bigint;
234
+ };
235
+ /**
236
+ * Builds the on-chain cooperative close for a session channel. It captures what the ledger records
237
+ * as consumed — never the full voucher, which may include refunded or unused value — and refunds
238
+ * the rest of the deposit to the payer. Submit the returned call from the payee account with your
239
+ * own wallet tooling; this package never holds keys.
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * const close = tempoSessionClose({ authorization, charges });
244
+ * await walletClient.sendTransaction({ to: close.to, data: close.data });
245
+ * ```
246
+ */
247
+ declare function tempoSessionClose(input: TempoSessionCloseInput): TempoSessionClose;
248
+
249
+ export { type MppStripeData, type MppStripeOptions, type MppStripeRail, type MppTempoData, type MppTempoOptions, type MppTempoRail, type MppTempoSessionData, type MppTempoSessionOptions, type MppTempoSessionRail, type TempoMode, type TempoSessionClose, type TempoSessionCloseInput, type TempoSplit, type TempoToken, mppStripe, mppTempo, mppTempoSession, tempoSessionClose };