From 7c55d43ca61b8ba06a82ec910a26bb1c79aa9cd6 Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:39:54 -0700 Subject: [PATCH] docs: sync mppx 0.13.1 --- .mppx-docs-sync | 4 +- package.json | 2 +- pnpm-lock.yaml | 30 ++-- src/pages/guides/use-mpp-with-x402.mdx | 6 +- src/pages/payment-methods/evm/charge.mdx | 4 +- src/pages/payment-methods/stripe/charge.mdx | 8 +- src/pages/payment-methods/stripe/index.mdx | 6 +- src/pages/payment-methods/tempo/session.mdx | 140 +----------------- src/pages/protocol/receipts.mdx | 3 +- src/pages/quickstart/server.mdx | 2 +- .../sdk/typescript/client/Method.tempo.mdx | 8 +- .../client/Method.tempo.session-manager.mdx | 10 +- .../client/Method.tempo.session.mdx | 91 +----------- .../sdk/typescript/core/Method.toServer.mdx | 4 +- .../sdk/typescript/core/Receipt.from.mdx | 6 + src/pages/sdk/typescript/index.mdx | 2 +- .../sdk/typescript/middlewares/elysia.mdx | 35 +++-- .../sdk/typescript/middlewares/express.mdx | 2 + src/pages/sdk/typescript/middlewares/hono.mdx | 2 + .../sdk/typescript/middlewares/nextjs.mdx | 2 + src/pages/sdk/typescript/proxy.mdx | 2 +- .../typescript/server/Method.evm.charge.mdx | 6 +- .../server/Method.stripe.create.mdx | 36 ++++- .../sdk/typescript/server/Method.stripe.mdx | 4 +- .../typescript/server/Method.tempo.charge.mdx | 6 +- .../sdk/typescript/server/Method.tempo.mdx | 2 +- .../server/Method.tempo.session.mdx | 58 +------- .../typescript/server/Mppx.toNodeListener.mdx | 7 + .../server/Request.toNodeListener.mdx | 20 ++- .../sdk/typescript/server/Transport.from.mdx | 38 ++++- src/pages/sdk/typescript/server/Ws.serve.mdx | 25 +++- 31 files changed, 213 insertions(+), 358 deletions(-) diff --git a/.mppx-docs-sync b/.mppx-docs-sync index 55a960466..03c80aeaa 100644 --- a/.mppx-docs-sync +++ b/.mppx-docs-sync @@ -5,5 +5,5 @@ # When updating docs from mppx changes, bump this SHA to HEAD of mppx main # after incorporating the new features/changes into the docs site. -mppx_version=0.12.0 -mppx_sha=e0b9f53f26e030fce54fc4c299db0c861b1e57b9 +mppx_version=0.13.1 +mppx_sha=44c4c8575c32df9e386dc757abbdce7141be781e diff --git a/package.json b/package.json index 709f7bae8..51654ef56 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "hono": "^4.13.9", "lottie-web": "^5.13.0", "mermaid": "^11.17.2", - "mppx": "0.12.0", + "mppx": "0.13.1", "nuqs": "2.9.1", "react": "~19.2.8", "react-dom": "~19.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6222f2ae1..5dd337d5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,8 +48,8 @@ importers: specifier: ^11.17.2 version: 11.17.2 mppx: - specifier: 0.12.0 - version: 0.12.0(@modelcontextprotocol/sdk@1.30.1(@cfworker/json-schema@4.1.1)(zod@4.6.5))(express@5.2.1)(hono@4.13.9)(typescript@6.0.3)(viem@2.57.1(typescript@6.0.3)(zod@4.6.5)) + specifier: 0.13.1 + version: 0.13.1(@modelcontextprotocol/sdk@1.30.1(@cfworker/json-schema@4.1.1)(zod@4.6.5))(express@5.2.1)(hono@4.13.9)(typescript@6.0.3)(viem@2.57.1(typescript@6.0.3)(zod@4.6.5)) nuqs: specifier: 2.9.1 version: 2.9.1(react@19.2.8) @@ -3107,6 +3107,10 @@ packages: resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} engines: {node: '>=18.0.0'} + eventsource-parser@4.1.1: + resolution: {integrity: sha512-zaWNqeSLeyzEAAt2Q6aDBRDRk/VnPmsKqvToyGvv6aahvLv71A4Jw7jG93Hckf11zXurbqgsffvpSgKPoeU1Qw==} + engines: {node: '>=22.12'} + eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} @@ -3954,8 +3958,8 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mppx@0.12.0: - resolution: {integrity: sha512-BEbDwmxOQUMImYQrkfAicD4qwgKHZkPOw8+NgiqJS5/DyFtyKX/0YK/Oz0y/LwyYGQn3KdOB6WmvwAWfOsR/rw==} + mppx@0.13.1: + resolution: {integrity: sha512-PKz2U3l74aqPbvhzLcPzntHWGCncC6czeH0k0aN/f8f9V60E5/nQq9msyoG425njU+UU/jhOh4Ug8gVv62rJQA==} hasBin: true peerDependencies: '@modelcontextprotocol/sdk': '>=1.25.0' @@ -3964,7 +3968,7 @@ packages: '@x402/hono': '>=2.22.0' '@x402/mcp': '>=2.22.0' '@x402/next': '>=2.22.0' - elysia: '>=1' + elysia: '>=1.2.0' express: '>=5' hono: '>=4.12.25' next: '>=16.2.6' @@ -4567,6 +4571,10 @@ packages: resolution: {integrity: sha512-4g5yxhlDMClRwCcfKfLeS7Z8yAVdOWGDADwm80Poh1iReU2KVKLGBlqwpHWJ2qovq0+ZIf1atAEO1eua2o9Rgg==} engines: {node: '>=18', npm: '>=6'} + structured-headers@2.1.0: + resolution: {integrity: sha512-ClzykN+XQVaSW5UfpOdEWfDplZ1Th7mISdaFvaC2+GjLTeBhGMJw83D/Rw9jPkHOml4oYd3EXaZhbz3gnE5nBQ==} + engines: {node: '>=18', npm: '>=6'} + style-mod@4.1.4: resolution: {integrity: sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==} @@ -8240,6 +8248,8 @@ snapshots: eventsource-parser@3.1.1: {} + eventsource-parser@4.1.1: {} + eventsource@3.0.7: dependencies: eventsource-parser: 3.1.1 @@ -9436,12 +9446,12 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 - mppx@0.12.0(@modelcontextprotocol/sdk@1.30.1(@cfworker/json-schema@4.1.1)(zod@4.6.5))(express@5.2.1)(hono@4.13.9)(typescript@6.0.3)(viem@2.57.1(typescript@6.0.3)(zod@4.6.5)): + mppx@0.13.1(@modelcontextprotocol/sdk@1.30.1(@cfworker/json-schema@4.1.1)(zod@4.6.5))(express@5.2.1)(hono@4.13.9)(typescript@6.0.3)(viem@2.57.1(typescript@6.0.3)(zod@4.6.5)): dependencies: - '@stripe/stripe-js': 9.13.0 - eventsource-parser: 3.1.1 + '@stripe/stripe-js': 9.17.0 + eventsource-parser: 4.1.1 ox: 0.14.45(typescript@6.0.3)(zod@4.6.5) - structured-headers: 2.0.3 + structured-headers: 2.1.0 viem: 2.57.1(typescript@6.0.3)(zod@4.6.5) zod: 4.6.5 optionalDependencies: @@ -10199,6 +10209,8 @@ snapshots: structured-headers@2.0.3: {} + structured-headers@2.1.0: {} + style-mod@4.1.4: {} style-to-js@1.1.21: diff --git a/src/pages/guides/use-mpp-with-x402.mdx b/src/pages/guides/use-mpp-with-x402.mdx index 6cec69e97..e54c43c97 100644 --- a/src/pages/guides/use-mpp-with-x402.mdx +++ b/src/pages/guides/use-mpp-with-x402.mdx @@ -197,9 +197,9 @@ This flow also supports body-bearing and route-scoped endpoints. Standard x402 c ### Advanced server options -#### Require route-bound x402 Credentials +#### Accept standard x402 Credentials -Keep the default `routeBinding: 'resource'` for compatibility with standard x402 clients. Set `routeBinding: 'required'` when every x402 Credential for a scoped route must include the `mppx` extension and a route-bound nonce. +Keep the default `routeBinding: 'required'` so every x402 Credential for a scoped route includes the `mppx` extension and a route-bound nonce. Set `routeBinding: 'resource'` only when scoped routes must accept standard x402 clients that don't implement the extension. ```ts twoslash [server.ts] import { evm } from 'mppx/server' @@ -210,7 +210,7 @@ const method = evm.charge({ // [!code hl:start] x402: { facilitator: 'https://x402.org/facilitator', - routeBinding: 'required', + routeBinding: 'resource', }, // [!code hl:end] }) diff --git a/src/pages/payment-methods/evm/charge.mdx b/src/pages/payment-methods/evm/charge.mdx index 55575a2fe..4e2bd96d2 100644 --- a/src/pages/payment-methods/evm/charge.mdx +++ b/src/pages/payment-methods/evm/charge.mdx @@ -131,9 +131,9 @@ const method = evm.charge({ ## x402 compatibility -When `x402.facilitator` is configured, the server returns MPP and x402 Challenges and accepts both MPP and x402 Credentials inline for `GET`, body-bearing, and route-scoped endpoints. The default `routeBinding: 'resource'` accepts standard x402 Credentials by comparing the echoed resource URL and payment requirements. It also verifies any body digest against the request. +When `x402.facilitator` is configured, the server returns MPP and x402 Challenges and accepts both MPP and x402 Credentials inline for `GET`, body-bearing, and route-scoped endpoints. The default `routeBinding: 'required'` requires the `mppx` extension and route-bound nonce whenever a Challenge includes route metadata. It also verifies any body digest against the request. -Set `routeBinding: 'required'` when every x402 Credential for a scoped route must include the `mppx` extension and route-bound nonce. This cryptographically binds MPP scope, opaque values, and metadata, but excludes standard clients that don't implement the extension from scoped routes. +Set `routeBinding: 'resource'` only when scoped routes must accept standard x402 clients that don't implement the extension. This compares the echoed resource URL and payment requirements, but those fields sit outside the EIP-3009 signature. On the client, `evm.charge` validates x402 offers before signing. Each offer must include resource information and EIP-3009 token name and version metadata, then pass the configured network, currency, and amount policies. The client skips unsupported or rejected offers and selects a later compatible offer when available. diff --git a/src/pages/payment-methods/stripe/charge.mdx b/src/pages/payment-methods/stripe/charge.mdx index 2912c617f..ec8fc3abf 100644 --- a/src/pages/payment-methods/stripe/charge.mdx +++ b/src/pages/payment-methods/stripe/charge.mdx @@ -22,15 +22,17 @@ Use [`stripe.create()`](/sdk/typescript/server/Method.stripe.create) to configur ```ts twoslash [server.ts] import Stripe from 'stripe' -import { Mppx, stripe } from 'mppx/server' +import { Mppx, Store, stripe } from 'mppx/server' const client = new Stripe(process.env.STRIPE_SECRET_KEY!) +declare const store: Store.AtomicStore const payments = stripe.create({ client, depositAddresses: (network) => stripe.findOrCreateDepositAddress(client, network), livemode: !process.env.STRIPE_SECRET_KEY!.includes('_test_'), networkId: process.env.STRIPE_PROFILE_ID!, + store, }) const mppx = Mppx.create({ @@ -52,6 +54,8 @@ export async function handler(request: Request) { The default handler accepts stablecoins on Tempo or cards and Link through an SPT. If a request doesn't include payment, the server returns a Challenge for each method in its `402` response. The client retries with a Credential for one method. `mppx` creates and confirms a Stripe `PaymentIntent` for an SPT, or records the stablecoin payment as a `PaymentIntent` after on-chain settlement. +The atomic `store` must be shared by every server instance so Stripe-backed Tempo methods use one replay-protection namespace. + If one asynchronous deposit-address lookup fails, `mppx` logs a warning and keeps the methods for networks that resolved. Omit `depositAddresses` when you want a synchronous SPT-only handler. :::note @@ -72,6 +76,7 @@ const payments = stripe.create({ livemode: !process.env.STRIPE_SECRET_KEY!.includes('_test_'), metadata: { plan: 'pro' }, networkId: process.env.STRIPE_PROFILE_ID!, + store, }) const result = await mppx.charge({ @@ -130,6 +135,7 @@ const payments = stripe.create({ hostedFeePayer: true, livemode: true, networkId: process.env.STRIPE_PROFILE_ID!, + store, }) ``` diff --git a/src/pages/payment-methods/stripe/index.mdx b/src/pages/payment-methods/stripe/index.mdx index db99fefc4..ef01c450d 100644 --- a/src/pages/payment-methods/stripe/index.mdx +++ b/src/pages/payment-methods/stripe/index.mdx @@ -30,15 +30,17 @@ Use [`stripe.create`](/sdk/typescript/server/Method.stripe.create) as the defaul ```ts twoslash [server.ts] import Stripe from 'stripe' -import { Mppx, stripe } from 'mppx/server' +import { Mppx, Store, stripe } from 'mppx/server' const client = new Stripe(process.env.STRIPE_SECRET_KEY!) +declare const store: Store.AtomicStore const payments = stripe.create({ client, depositAddresses: (network) => stripe.findOrCreateDepositAddress(client, network), livemode: false, networkId: process.env.STRIPE_NETWORK_ID!, + store, }) const mppx = Mppx.create({ @@ -47,7 +49,7 @@ const mppx = Mppx.create({ }) ``` -The resolver runs only for the stablecoin networks you enable. If one network fails, `mppx` warns and keeps the other resolved methods. Omit `depositAddresses` for a synchronous SPT-only setup. +The resolver runs only for the stablecoin networks you enable. If one network fails, `mppx` warns and keeps the other resolved methods. Omit `depositAddresses` for a synchronous SPT-only setup. The atomic `store` must be shared by every server instance so Stripe-backed Tempo methods use one replay-protection namespace. Add a Tempo Session method through `defaultMethods().additional({ tempo: { session: {} } })`. Each on-chain Session settlement of at least one cent records its newly settled amount in Stripe before `mppx` calls your optional `onSessionSettlement` hook. `mppx` skips recording when the amount rounds down below one cent. diff --git a/src/pages/payment-methods/tempo/session.mdx b/src/pages/payment-methods/tempo/session.mdx index 7e825d18d..5a403aee7 100644 --- a/src/pages/payment-methods/tempo/session.mdx +++ b/src/pages/payment-methods/tempo/session.mdx @@ -20,7 +20,7 @@ The `session` intent enables high-frequency, pay-as-you-go payments over unidire Payment sessions reduce payment verification to near constant time, making it possible to meter and bill at the granularity of individual LLM tokens, API calls, or bytes transferred. :::warning[Legacy integrations] -`tempo.session` is the current Sessions implementation in `mppx`. The previous contract-backed implementation is Legacy Sessions, also called Sessions v1, and is available as `tempo.sessionLegacy`. Legacy server support requires `mppx` 0.8.15 or earlier. +`mppx` 0.13.0 removed the deprecated Legacy Sessions client exports. Close or settle Legacy channels before upgrading because current TIP-1034 Sessions don't share their channel state. ::: ## Which client API should I use? @@ -32,7 +32,6 @@ There are two current Sessions client APIs: | `tempo({ account, maxDeposit })` | You want a fetch wrapper that handles both one-time charges and Sessions. This expands to `tempo.charge()` plus the current `tempo.session()` client method. | | `tempo.session({ account, maxDeposit })` | You want to register only the current Sessions client method in `Mppx.create`. | | `tempo.session.manager({ account, maxDeposit })` | You want direct lifecycle control with `.fetch()`, `.topUp()`, `.close()`, `.sse()`, or `.ws()`. Use this when your code must explicitly close or top up a channel. | -| `tempo.sessionLegacy` / `tempo.sessionLegacy.method()` | You still need compatibility with contract-backed Sessions v1. Do not use this for new integrations. | For browser reloads or app restarts, pass a [`channelStore`](/sdk/typescript/client/Method.tempo.session-manager#channelstore) to `tempo.session.manager()`. Servers can pair this with [`bootstrap: true`](/sdk/typescript/server/Method.tempo.session#with-same-route-bootstrap) so clients lazily recover a previous channel from the same protected route before opening a new one. @@ -89,6 +88,8 @@ If the channel runs low on funds, the client tops up the channel without closing Either party can close the channel. The server closes the precompile-backed channel with the highest voucher, settling the final balance on-chain and refunding any unused deposit to the client. +A cooperative close Credential includes the latest voucher signature and a separate `closeSignature` that authorizes the close action. `tempo.session.manager()` creates both automatically. Custom protocol clients can use `Session.Precompile.Voucher.signCloseAuthorization()` from `mppx/tempo`; servers can verify it with `verifyCloseAuthorization()`. + :::: ## Session Receipts @@ -513,136 +514,11 @@ See [`tempo.session.manager`](/sdk/typescript/client/Method.tempo.session-manage ## Migrate from Legacy Sessions -Legacy Sessions, also called Sessions v1, is the contract-backed session flow. Use `tempo.sessionLegacy` only when you need compatibility with clients or servers that haven't moved to the latest implementation. Legacy server support requires `mppx` 0.8.15 or earlier. - -- Register `tempo.session` on the server for the latest implementation. -- Keep `tempo.sessionLegacy` registered beside `tempo.session` during migration so existing clients keep working. -- Use `tempo()` on the client when the same fetch wrapper should handle charges and Sessions. -- Register `tempo.session()` and `tempo.sessionLegacy.method()` explicitly when the client must support both Sessions implementations. - -### Compatibility matrix - -| Server methods | Client methods | Result | -|---|---|---| -| `tempo.session()` only | `tempo.session()` or `tempo()` | Current Sessions flow. New integrations should target this. | -| `tempo.sessionLegacy()` only | `tempo.sessionLegacy()` or `tempo.sessionLegacy.method()` | Legacy Sessions v1 flow. Use only until the server migrates. | -| `tempo.session()` only | `tempo.sessionLegacy()` or `tempo.sessionLegacy.method()` | Not compatible. The client cannot answer current Sessions Challenges. | -| `tempo.sessionLegacy()` only | `tempo.session()` or `tempo()` | Not compatible for Sessions. The client cannot answer Legacy Sessions Challenges unless `tempo.sessionLegacy.method()` is also registered. | -| `tempo.session()` and `tempo.sessionLegacy()` | `tempo.session()` and `tempo.sessionLegacy.method()` | Migration mode. Both current and Legacy Sessions Challenges can be handled while clients roll forward. | - -Current Sessions Challenges advertise `sessionProtocol: "v2"` in method details and use TIP-1034 reserve channels. Legacy Sessions Challenges use the contract-backed Sessions v1 flow. Channel state is not reusable across implementations; let old channels close or settle under `tempo.sessionLegacy`, and open new channels with `tempo.session`. - -### Server - -Pin `mppx` to 0.8.15 or earlier for this migration configuration. - -```ts -import { Mppx, Store, tempo } from 'mppx/server' -import { privateKeyToAccount } from 'viem/accounts' - -const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef') - -const mppx = Mppx.create({ - methods: [ - // Keep both registered during migration so current and Legacy Sessions clients work. - // [!code hl:start] - tempo.session({ - account, - chainId: 4217, // optional; pins Challenges to Tempo mainnet - currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo - store: Store.memory(), - }), - tempo.sessionLegacy({ - account, - currency: '0x20c0000000000000000000006a37DA5C996874BE', // OUSD on Tempo - store: Store.memory(), - }), - // [!code hl:end] - ], -}) -``` - -### Client - - - - -```ts twoslash -import { Mppx, tempo } from 'mppx/client' -import { createClient, http } from 'viem' -import { Provider } from 'accounts' -import { tempo as tempoMainnet } from 'viem/chains' - -const provider = Provider.create({ mpp: false }) // Avoid double 402 handling; mppx is configured below. -await provider.request({ method: 'wallet_connect' }) - -const { fetch: mppxFetch } = Mppx.create({ - methods: [ - tempo.session({ - account: provider.getAccount({ signable: true }), - getClient: () => - createClient({ - chain: tempoMainnet, - transport: http('https://rpc.tempo.xyz'), - }), - maxDeposit: '1', - }), - tempo.sessionLegacy.method({ - account: provider.getAccount({ signable: true }), - getClient: () => - createClient({ - chain: tempoMainnet, - transport: http('https://rpc.tempo.xyz'), - }), - maxDeposit: '1', - }), - ], - polyfill: false, -}) - -const response = await mppxFetch('https://api.example.com/resource') -``` +Legacy Sessions, also called Sessions v1, used a contract-backed channel flow. `mppx` 0.13.0 removes its deprecated client exports, and current Sessions use TIP-1034 reserve channels. - - - -```ts twoslash -import { Mppx, tempo } from 'mppx/client' -import { createClient, http } from 'viem' -import { privateKeyToAccount } from 'viem/accounts' -import { tempo as tempoMainnet } from 'viem/chains' - -const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef') - -const { fetch: mppxFetch } = Mppx.create({ - methods: [ - tempo.session({ - account, - getClient: () => - createClient({ - chain: tempoMainnet, - transport: http('https://rpc.tempo.xyz'), - }), - maxDeposit: '1', - }), - tempo.sessionLegacy.method({ - account, - getClient: () => - createClient({ - chain: tempoMainnet, - transport: http('https://rpc.tempo.xyz'), - }), - maxDeposit: '1', - }), - ], - polyfill: false, -}) - -const response = await mppxFetch('https://api.example.com/resource') -``` - - - +- Close or settle Legacy channels before upgrading. +- Upgrade servers to `tempo.session()` and clients to `tempo.session()` or `tempo()`. +- Use `tempo.session.manager()` when application code needs explicit channel lifecycle control. ## Custom reserve contracts @@ -673,8 +549,6 @@ const manager = tempo.session.manager({ For a stricter policy, set the client `escrow` option to the exact address you trust. This pin takes precedence over `allowCustomEscrow`. Persisted channels remain bound to their resolved reserve address, so a later Challenge can't switch an existing channel. -Legacy Sessions clients use the same `allowCustomEscrow` opt-in. Their `escrowContract` option is the exact-address pin. - ## Reserve precompile Sessions use the [TIP-1034 precompile](https://tips.sh/1034) for on-chain deposits, settlement, top-ups, and channel close. The IETF Specification documents the voucher format and HTTP authentication flow. diff --git a/src/pages/protocol/receipts.mdx b/src/pages/protocol/receipts.mdx index 419772cfb..9701dfae9 100644 --- a/src/pages/protocol/receipts.mdx +++ b/src/pages/protocol/receipts.mdx @@ -40,13 +40,14 @@ The Receipt is a base64url-encoded JSON object. | Field | Description | |-------|-------------| | `externalId` | Optional external reference echoed from the Credential payload | +| `fundingCurrency` | Optional currency actually debited to fund the payment | | `method` | Payment method used | | `reference` | Method-specific payment reference (for example, transaction hash or invoice ID) | | `status` | Payment outcome (`success`) | | `subscriptionId` | Optional server-issued subscription identifier | | `timestamp` | When the payment was processed | -Payment method specifications can define additional fields. +Payment method specifications can define additional fields. Tempo charge Receipts include `fundingCurrency` when `mppx` can verify the direct, machine-token, or auto-swap funding route. ## Use cases diff --git a/src/pages/quickstart/server.mdx b/src/pages/quickstart/server.mdx index 2386eac9f..17ebdf24b 100644 --- a/src/pages/quickstart/server.mdx +++ b/src/pages/quickstart/server.mdx @@ -93,7 +93,7 @@ const mppx = Mppx.create({ const app = new Elysia() .guard( - { beforeHandle: mppx.charge({ amount: '0.1' }) }, // [!code hl] + mppx.charge({ amount: '0.1' }), // [!code hl] (app) => app.get('/resource', () => ({ data: '...' })), ) ``` diff --git a/src/pages/sdk/typescript/client/Method.tempo.mdx b/src/pages/sdk/typescript/client/Method.tempo.mdx index 9e8a6fc25..9fff00d9d 100644 --- a/src/pages/sdk/typescript/client/Method.tempo.mdx +++ b/src/pages/sdk/typescript/client/Method.tempo.mdx @@ -22,9 +22,7 @@ Register [`tempo.subscription`](/sdk/typescript/client/Method.tempo.subscription If you need to close, top up, or stream a single channel explicitly, use [`tempo.session.manager()`](/sdk/typescript/client/Method.tempo.session-manager) instead. -:::warning[Legacy Sessions] -`tempo.session` is the current Sessions implementation. The previous contract-backed flow is Legacy Sessions, also called Sessions v1, and is available as `tempo.sessionLegacy`. -::: +`mppx` 0.13.0 removed the deprecated Legacy Sessions client exports. Use `tempo.session` for TIP-1034 Sessions. ## Usage @@ -204,9 +202,9 @@ Store for reusable Sessions channels. Defaults to an in-memory store. - **Type:** `number` -Tempo chain ID this client accepts for charges. When set, the client rejects Challenges for other chains and uses this chain when a charge Challenge omits `chainId`. +Tempo chain ID this client accepts for both generated methods. Charge payments use it as an exact chain pin. Sessions receive the equivalent one-chain allowlist. -Use `allowedChainIds` to enforce the same policy for both generated methods. When both options are set, charge payments must satisfy both. +When `allowedChainIds` is also set, it must include `expectedChainId`; otherwise the generated Session method rejects every chain. ### getClient (optional) diff --git a/src/pages/sdk/typescript/client/Method.tempo.session-manager.mdx b/src/pages/sdk/typescript/client/Method.tempo.session-manager.mdx index 41d91102f..e728e0338 100644 --- a/src/pages/sdk/typescript/client/Method.tempo.session-manager.mdx +++ b/src/pages/sdk/typescript/client/Method.tempo.session-manager.mdx @@ -15,9 +15,7 @@ Use this when application code needs direct lifecycle control for one channel. T Use `tempo.session()` instead when you only need to register the current Sessions method inside `Mppx.create`. Use `tempo()` when the same fetch wrapper should handle both one-time charges and current Sessions. -:::warning[Legacy Sessions] -`tempo.session.manager` is the current Sessions manager. The previous contract-backed manager is Legacy Sessions, also called Sessions v1, and is available as `tempo.sessionLegacy`. -::: +`mppx` 0.13.0 removed the deprecated Legacy Sessions manager exports. ## Usage @@ -392,9 +390,9 @@ When `bootstrap: true` is enabled, the manager first sends a same-route `HEAD` r ## Migrate from Legacy Sessions -Legacy Sessions clients used `tempo.sessionLegacy()` or `tempo.sessionLegacy.method()`. Use `tempo.session.manager()` for a standalone manager, or `tempo()` when you want the same fetch wrapper to handle one-time charges and Sessions. +Legacy Sessions clients used `tempo.sessionLegacy()` or `tempo.sessionLegacy.method()` in `mppx` 0.12.0 and earlier. Close or settle existing Legacy channels before upgrading because current Sessions don't share their channel state. -Current Sessions and Legacy Sessions do not share channel state. Existing Legacy channels should be closed or settled through `tempo.sessionLegacy`; new channels should be opened through `tempo.session` or `tempo.session.manager()`. During a rolling migration, register both `tempo.session()` and `tempo.sessionLegacy.method()` if the client may talk to both server versions. +Use `tempo.session.manager()` for direct lifecycle control, or `tempo()` when the same fetch wrapper handles one-time charges and Sessions. @@ -458,8 +456,6 @@ const response = await mppxFetch('https://api.example.com/resource') -Use `tempo.sessionLegacy()` only while the server still emits Legacy Sessions Challenges. - ## Return type `tempo.session.manager()` returns a `SessionManager` object: diff --git a/src/pages/sdk/typescript/client/Method.tempo.session.mdx b/src/pages/sdk/typescript/client/Method.tempo.session.mdx index 0047f7d3e..6b8d762b0 100644 --- a/src/pages/sdk/typescript/client/Method.tempo.session.mdx +++ b/src/pages/sdk/typescript/client/Method.tempo.session.mdx @@ -15,9 +15,7 @@ Creates the low-level Tempo Sessions client method for `Mppx.create`. Use `tempo.session()` when you need to register only the current Sessions intent in `Mppx.create`. Use [`tempo.session.manager()`](/sdk/typescript/client/Method.tempo.session-manager) when you need a standalone object with `.fetch()`, `.topUp()`, `.close()`, `.sse()`, or `.ws()`. ::: -:::warning[Legacy Sessions] -`tempo.session` is the current Sessions implementation, backed by the [TIP-1034](https://tips.sh/1034) reserve precompile. The previous contract-backed implementation is Legacy Sessions, also called Sessions v1, and is available as `tempo.sessionLegacy`. -::: +`tempo.session` uses the [TIP-1034](https://tips.sh/1034) reserve precompile. `mppx` 0.13.0 removed the deprecated Legacy Sessions client exports. ## Usage @@ -104,93 +102,6 @@ Mppx.create({ -### With Legacy Sessions - -Use `tempo.sessionLegacy.method()` only for servers that still issue Legacy Sessions Challenges. - -If a client must support both current and Legacy Sessions during migration, register both methods: - - - - -```ts twoslash -import { Mppx, tempo } from 'mppx/client' -import { Provider } from 'accounts' - -const provider = Provider.create({ mpp: false }) // Avoid double 402 handling; mppx is configured below. -await provider.request({ method: 'wallet_connect' }) - -Mppx.create({ - methods: [ - tempo.session({ - account: provider.getAccount({ signable: true }), - getClient: provider.getClient, - maxDeposit: '1', - }), - tempo.sessionLegacy.method({ - account: provider.getAccount({ signable: true }), - getClient: provider.getClient, - maxDeposit: '1', - }), - ], -}) -``` - - - - -```ts twoslash -import { Mppx, tempo } from 'mppx/client' -import { privateKeyToAccount } from 'viem/accounts' - -const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef') - -Mppx.create({ - methods: [ - tempo.session({ account, maxDeposit: '1' }), - tempo.sessionLegacy.method({ account, maxDeposit: '1' }), - ], -}) -``` - - - - - - - -```ts twoslash -import { Mppx, tempo } from 'mppx/client' -import { Provider } from 'accounts' - -const provider = Provider.create({ mpp: false }) // Avoid double 402 handling; mppx is configured below. -await provider.request({ method: 'wallet_connect' }) - -Mppx.create({ - methods: [tempo.sessionLegacy.method({ - account: provider.getAccount({ signable: true }), - getClient: provider.getClient, - })], -}) -``` - - - - -```ts twoslash -import { Mppx, tempo } from 'mppx/client' -import { privateKeyToAccount } from 'viem/accounts' - -const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef') - -Mppx.create({ - methods: [tempo.sessionLegacy.method({ account })], -}) -``` - - - - ## Automatic open retries When `Fetch.from` or `Mppx.create` opens a channel automatically, it keeps the new channel provisional until the server accepts the response or acknowledges the open with a matching Receipt or Session snapshot. A rejected or unacknowledged open is discarded, so the next paid request retries it. Concurrent automatic opens for the same payment scope and channel store are serialized. diff --git a/src/pages/sdk/typescript/core/Method.toServer.mdx b/src/pages/sdk/typescript/core/Method.toServer.mdx index 084bac5c0..a5865fa5d 100644 --- a/src/pages/sdk/typescript/core/Method.toServer.mdx +++ b/src/pages/sdk/typescript/core/Method.toServer.mdx @@ -137,9 +137,9 @@ Override the transport for this method. #### validate (optional) -- **Type:** `(parameters: { credential: Credential; request: request }) => Promise` +- **Type:** `(parameters: { credential: Credential; operation?: 'broadcast' | 'validate'; request: request }) => Promise` -Validates a Credential without settling, reserving, broadcasting, or otherwise consuming payment state. +Validates a Credential without settling, reserving, broadcasting, or otherwise consuming payment state. `operation` is `'validate'` for standalone validation and `'broadcast'` when validation immediately precedes the terminal broadcast hook. #### verify (deprecated) diff --git a/src/pages/sdk/typescript/core/Receipt.from.mdx b/src/pages/sdk/typescript/core/Receipt.from.mdx index 67cb2adbc..90fb58868 100644 --- a/src/pages/sdk/typescript/core/Receipt.from.mdx +++ b/src/pages/sdk/typescript/core/Receipt.from.mdx @@ -31,6 +31,12 @@ A validated Receipt object. External reference ID echoed from the Credential payload. +### fundingCurrency (optional) + +- **Type:** `string` + +Currency actually debited to fund the payment, when known. + ### method - **Type:** `string` diff --git a/src/pages/sdk/typescript/index.mdx b/src/pages/sdk/typescript/index.mdx index caf23292f..dbbdc5f9d 100644 --- a/src/pages/sdk/typescript/index.mdx +++ b/src/pages/sdk/typescript/index.mdx @@ -320,7 +320,7 @@ const mppx = Mppx.create({ const app = new Elysia() .guard( - { beforeHandle: mppx.charge({ amount: '0.1' }) }, // [!code hl] + mppx.charge({ amount: '0.1' }), // [!code hl] (app) => app.get('/resource', () => ({ data: '...' })), ) ``` diff --git a/src/pages/sdk/typescript/middlewares/elysia.mdx b/src/pages/sdk/typescript/middlewares/elysia.mdx index 80b8142f8..b425e2fdf 100644 --- a/src/pages/sdk/typescript/middlewares/elysia.mdx +++ b/src/pages/sdk/typescript/middlewares/elysia.mdx @@ -13,7 +13,7 @@ import { SigningAccountTabs } from '../../../../components/SigningAccountTabs' -Native [Elysia](https://elysiajs.com) middleware that gates routes behind payment intents. +Native [Elysia](https://elysiajs.com) middleware that gates routes behind payment intents. Requires Elysia 1.2.0 or newer. ## Install @@ -35,7 +35,7 @@ $ bun add mppx elysia ## Usage -Import `Mppx` and `tempo` from `mppx/elysia` to create an Elysia-aware payment handler. Each intent (for example, `charge`) returns an Elysia `beforeHandle` hook you can use with `.guard()` to scope payment to specific routes. +Import `Mppx` and `tempo` from `mppx/elysia` to create an Elysia-aware payment handler. Each intent returns paired `beforeHandle` and `afterHandle` hooks. Pass the complete result to `.guard()` so `mppx` verifies the Credential before the route and attaches the Receipt to the actual route response. ```ts [server.ts] import { Elysia } from 'elysia' @@ -45,14 +45,18 @@ const mppx = Mppx.create({ methods: [tempo.charge()] }) const app = new Elysia() .guard( - { beforeHandle: mppx.charge({ amount: '1' }) }, + mppx.charge({ amount: '1' }), (app) => app.get('/premium', () => ({ data: 'paid content' })), ) ``` -### Global application +Don't register only the `beforeHandle` function. Streaming payments require the paired `afterHandle` hook to meter the route response. -Use `.onBeforeHandle()` to apply payment to all routes. +When route options omit `scope` and reserved scope metadata, the middleware binds the Challenge to the Elysia method and route path automatically. + +### Protect multiple routes + +Put multiple routes inside the same `.guard()` callback to apply one payment configuration to all of them. ```ts [server.ts] import { Elysia } from 'elysia' @@ -61,9 +65,12 @@ import { Mppx, tempo } from 'mppx/elysia' const mppx = Mppx.create({ methods: [tempo.charge()] }) // [!code hl] const app = new Elysia() - .onBeforeHandle(mppx.charge({ amount: '1' })) // [!code hl] - .get('/premium', () => ({ data: 'paid content' })) - .get('/another', () => ({ data: 'also paid' })) + .guard( + mppx.charge({ amount: '1' }), // [!code hl] + (app) => app + .get('/premium', () => ({ data: 'paid content' })) + .get('/another', () => ({ data: 'also paid' })), + ) ``` ### Session payments @@ -91,7 +98,7 @@ const mppx = Mppx.create({ const app = new Elysia() .guard( - { beforeHandle: mppx.session({ amount: '1', unitType: 'token' }) }, + mppx.session({ amount: '1', unitType: 'token' }), (app) => app.get('/content', () => ({ data: 'session content' })), ) ``` @@ -118,12 +125,10 @@ const mppx = Mppx.create({ }) const app = new Elysia().guard( - { - beforeHandle: mppx.evm.charge({ - amount: '0.01', - description: 'Premium API access', - }), - }, + mppx.evm.charge({ + amount: '0.01', + description: 'Premium API access', + }), (app) => app.get('/paid', () => ({ data: 'paid content' })), ) ``` diff --git a/src/pages/sdk/typescript/middlewares/express.mdx b/src/pages/sdk/typescript/middlewares/express.mdx index bfc19be45..0bf74fce6 100644 --- a/src/pages/sdk/typescript/middlewares/express.mdx +++ b/src/pages/sdk/typescript/middlewares/express.mdx @@ -37,6 +37,8 @@ $ bun add mppx express Import `Mppx` and `tempo` from `mppx/express` to create an Express-aware payment handler. Each intent (for example, `charge`) returns an Express `RequestHandler` you can slot directly into your route. +When route options omit `scope` and reserved scope metadata, the middleware binds the Challenge to the Express method and route path automatically. + ```ts [server.ts] import express from 'express' import { Mppx, tempo } from 'mppx/express' diff --git a/src/pages/sdk/typescript/middlewares/hono.mdx b/src/pages/sdk/typescript/middlewares/hono.mdx index 7e603821d..c71bce7b2 100644 --- a/src/pages/sdk/typescript/middlewares/hono.mdx +++ b/src/pages/sdk/typescript/middlewares/hono.mdx @@ -37,6 +37,8 @@ $ bun add mppx hono Import `Mppx` and `tempo` from `mppx/hono` to create a Hono-aware payment handler. Each intent (for example, `charge`) returns a Hono `MiddlewareHandler` you can slot directly into your route. +When route options omit `scope` and reserved scope metadata, the middleware binds the Challenge to the Hono method and route path automatically. + ```ts twoslash [server.ts] import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' diff --git a/src/pages/sdk/typescript/middlewares/nextjs.mdx b/src/pages/sdk/typescript/middlewares/nextjs.mdx index 525302a97..635c82ad8 100644 --- a/src/pages/sdk/typescript/middlewares/nextjs.mdx +++ b/src/pages/sdk/typescript/middlewares/nextjs.mdx @@ -37,6 +37,8 @@ $ bun add mppx Import `Mppx` and `tempo` from `mppx/nextjs` to create a Next.js-aware payment handler. Each intent (for example, `charge`) returns a wrapper that accepts a route handler. +When route options omit `scope` and reserved scope metadata, the wrapper binds the Challenge to the request method and pathname automatically. + ```ts twoslash [app/api/premium/route.ts] import { Mppx, tempo } from 'mppx/nextjs' diff --git a/src/pages/sdk/typescript/proxy.mdx b/src/pages/sdk/typescript/proxy.mdx index f38f374c6..ae35c5078 100644 --- a/src/pages/sdk/typescript/proxy.mdx +++ b/src/pages/sdk/typescript/proxy.mdx @@ -67,7 +67,7 @@ Route values use the current `EndpointMap` shape: - Use `true` for free passthrough routes. - Use a method-specific handler, such as `mppx.tempo.session({ amount, unitType })`, for session-priced routes. -`Proxy` derives a scope for each route. EVM methods with x402 enabled accept standard x402 Credentials on these scoped routes by default, using resource URL binding. Set [`x402.routeBinding: 'required'`](/sdk/typescript/server/Method.evm.charge#x402-optional) when only extension-aware clients can pay. +`Proxy` derives a scope for each route. EVM methods with x402 enabled require the `mppx` route-binding extension on these scoped routes by default. Set [`x402.routeBinding: 'resource'`](/sdk/typescript/server/Method.evm.charge#x402-optional) only when standard x402 clients must pay through resource URL binding. ### Multiple services diff --git a/src/pages/sdk/typescript/server/Method.evm.charge.mdx b/src/pages/sdk/typescript/server/Method.evm.charge.mdx index fa6b261d2..a3e394b3d 100644 --- a/src/pages/sdk/typescript/server/Method.evm.charge.mdx +++ b/src/pages/sdk/typescript/server/Method.evm.charge.mdx @@ -123,10 +123,10 @@ x402 compatibility options. Pass `facilitator` to verify and settle x402 exact p `routeBinding` controls scoped-route interoperability: -- **`'resource'`** (default)—Accept standard x402 Credentials by comparing the echoed resource URL and payment requirements. Credentials with the `mppx` extension retain full route-bound nonce verification. -- **`'required'`**—Require every x402 Credential for a scoped route to include the `mppx` extension and route-bound nonce. Use this when scope, opaque values, or metadata must be cryptographically bound. +- **`'required'`** (default)—Require every x402 Credential for a scoped route to include the `mppx` extension and route-bound nonce. Use this when scope, opaque values, or metadata must be cryptographically bound. +- **`'resource'`**—Accept standard x402 Credentials by comparing the echoed resource URL and payment requirements. Credentials with the `mppx` extension retain full route-bound nonce verification. -Both modes verify any Challenge body digest against the request. Standard x402 Credentials can pay `Proxy` routes under the default mode because the proxy's derived scope no longer requires the optional extension. +Both modes verify any Challenge body digest against the request. Standard x402 Credentials can pay scoped `Proxy` routes only when you set `routeBinding: 'resource'`. ## Request parameters diff --git a/src/pages/sdk/typescript/server/Method.stripe.create.mdx b/src/pages/sdk/typescript/server/Method.stripe.create.mdx index b1259779e..c7a1e54d0 100644 --- a/src/pages/sdk/typescript/server/Method.stripe.create.mdx +++ b/src/pages/sdk/typescript/server/Method.stripe.create.mdx @@ -13,9 +13,10 @@ Pass a deposit-address resolver, then use `defaultMethods()` to offer Tempo stab ```ts twoslash [server.ts] import Stripe from 'stripe' -import { Mppx, stripe } from 'mppx/server' +import { Mppx, Store, stripe } from 'mppx/server' const client = new Stripe(process.env.STRIPE_SECRET_KEY!) +declare const store: Store.AtomicStore const payments = stripe.create({ client, @@ -23,6 +24,7 @@ const payments = stripe.create({ livemode: false, metadata: { plan: 'pro' }, networkId: process.env.STRIPE_NETWORK_ID!, + store, }) const mppx = Mppx.create({ @@ -41,7 +43,7 @@ export async function handler(request: Request) { } ``` -Test mode uses pathUSD on Tempo testnet. Live mode uses USDC.e on Tempo mainnet. The SPT method defaults to cards and Link in both modes. +Test mode uses pathUSD on Tempo testnet. Live mode uses USDC.e on Tempo mainnet. The SPT method defaults to cards and Link in both modes. The atomic `store` must be shared by every server instance so Stripe-backed Tempo payments use one replay-protection namespace. ### Configure one PaymentIntent @@ -49,14 +51,16 @@ Pass `paymentIntentOptions` with route options to associate a Stripe Customer, a ```ts twoslash [server.ts] import Stripe from 'stripe' -import { Mppx, stripe } from 'mppx/server' +import { Mppx, Store, stripe } from 'mppx/server' const client = new Stripe(process.env.STRIPE_SECRET_KEY!) +declare const store: Store.AtomicStore const payments = stripe.create({ client, livemode: false, metadata: { plan: 'pro' }, networkId: process.env.STRIPE_NETWORK_ID!, + store, }) const mppx = Mppx.create({ methods: payments.defaultMethods(), @@ -83,7 +87,7 @@ Pass a resolver when options depend on the verified Credential or canonical requ ```ts twoslash [server.ts] import Stripe from 'stripe' -import { Mppx, stripe } from 'mppx/server' +import { Mppx, Store, stripe } from 'mppx/server' declare function findOrCreateTaxCalculation(parameters: { amount: unknown @@ -91,10 +95,12 @@ declare function findOrCreateTaxCalculation(parameters: { }): Promise const client = new Stripe(process.env.STRIPE_SECRET_KEY!) +declare const store: Store.AtomicStore const payments = stripe.create({ client, livemode: false, networkId: process.env.STRIPE_NETWORK_ID!, + store, }) const mppx = Mppx.create({ methods: payments.defaultMethods(), @@ -135,14 +141,16 @@ Add a Tempo Session method through `defaultMethods().additional()`. Each settlem ```ts twoslash [server.ts] import Stripe from 'stripe' -import { Mppx, stripe } from 'mppx/server' +import { Mppx, Store, stripe } from 'mppx/server' const client = new Stripe(process.env.STRIPE_SECRET_KEY!) +declare const store: Store.AtomicStore const payments = stripe.create({ client, depositAddresses: (network) => stripe.findOrCreateDepositAddress(client, network), livemode: true, networkId: process.env.STRIPE_NETWORK_ID!, + store, }) const mppx = Mppx.create({ @@ -161,9 +169,10 @@ Set `hostedFeePayer: true` to use Stripe's hosted Tempo fee payer for charge and ```ts twoslash [server.ts] import Stripe from 'stripe' -import { stripe } from 'mppx/server' +import { Store, stripe } from 'mppx/server' const client = new Stripe(process.env.STRIPE_SECRET_KEY!) +declare const store: Store.AtomicStore const payments = stripe.create({ client, @@ -171,6 +180,7 @@ const payments = stripe.create({ hostedFeePayer: true, livemode: true, networkId: process.env.STRIPE_NETWORK_ID!, + store, }) ``` @@ -186,14 +196,16 @@ Exclude `spt` or `tempo` when you don't want both default charge methods. ```ts twoslash [server.ts] import Stripe from 'stripe' -import { Mppx, stripe } from 'mppx/server' +import { Mppx, Store, stripe } from 'mppx/server' const client = new Stripe(process.env.STRIPE_SECRET_KEY!) +declare const store: Store.AtomicStore const payments = stripe.create({ client, depositAddresses: (network) => stripe.findOrCreateDepositAddress(client, network), livemode: false, networkId: process.env.STRIPE_NETWORK_ID!, + store, }) const mppx = Mppx.create({ @@ -208,9 +220,10 @@ Provide deposit addresses to create the default methods synchronously and avoid ```ts twoslash [server.ts] import Stripe from 'stripe' -import { Mppx, stripe } from 'mppx/server' +import { Mppx, Store, stripe } from 'mppx/server' const client = new Stripe(process.env.STRIPE_SECRET_KEY!) +declare const store: Store.AtomicStore const payments = stripe.create({ client, @@ -219,6 +232,7 @@ const payments = stripe.create({ }, livemode: true, networkId: process.env.STRIPE_NETWORK_ID!, + store, }) const mppx = Mppx.create({ @@ -329,3 +343,9 @@ Key-value pairs attached to Stripe `PaymentIntent` objects created or recorded b - **Type:** `string` Stripe Business Network profile ID used by the SPT method. + +### store + +- **Type:** `Store.AtomicStore` + +Atomic replay store shared by every server instance that uses this Stripe integration. `stripe.create()` passes it to every Stripe-backed Tempo charge and Session method and rejects per-method store overrides. Use `Store.redis()`, `Store.upstash()`, or `Store.cloudflare()` in multi-instance deployments. diff --git a/src/pages/sdk/typescript/server/Method.stripe.mdx b/src/pages/sdk/typescript/server/Method.stripe.mdx index 96f42f6bf..60135bce3 100644 --- a/src/pages/sdk/typescript/server/Method.stripe.mdx +++ b/src/pages/sdk/typescript/server/Method.stripe.mdx @@ -13,15 +13,17 @@ Prefer `stripe.create()` so the factory is explicit in application code. ```ts twoslash [server.ts] import Stripe from 'stripe' -import { Mppx, stripe } from 'mppx/server' +import { Mppx, Store, stripe } from 'mppx/server' const client = new Stripe(process.env.STRIPE_SECRET_KEY!) +declare const store: Store.AtomicStore const payments = stripe({ client, depositAddresses: (network) => stripe.findOrCreateDepositAddress(client, network), livemode: false, networkId: process.env.STRIPE_NETWORK_ID!, + store, }) const mppx = Mppx.create({ diff --git a/src/pages/sdk/typescript/server/Method.tempo.charge.mdx b/src/pages/sdk/typescript/server/Method.tempo.charge.mdx index 1f6744e5b..15e3ae97f 100644 --- a/src/pages/sdk/typescript/server/Method.tempo.charge.mdx +++ b/src/pages/sdk/typescript/server/Method.tempo.charge.mdx @@ -233,6 +233,8 @@ type ReturnType = readonly [Method.Server, ...Method.Server[]] Code that inspects a single method must destructure the group. For route composition, use the configured `mppx.tempo.charge` handler. +Successful charge Receipts include `fundingCurrency` when `mppx` can verify which token funded a direct, MACH, or auto-swap payment route. + ## Configuration These parameters configure the `tempo.charge()` constructor. @@ -302,7 +304,7 @@ This setting only applies to non-zero charges. Zero-amount proof flows do not cr Override the local fee-sponsor policy used when the server co-signs Tempo charge transactions. Remote fee payer services enforce their own policy. -`allowKeyAuthorization` defaults to `true`. Set it to `false` when this sponsor must reject transactions that install a new access key. +`allowKeyAuthorization` defaults to `false`. Set it to `true` only when this sponsor may fund transactions that install a new access key. `mppx` resolves the remaining defaults per chain automatically. On mainnet (`4217`), the defaults are `maxFeePerGas: 100_000_000_000n`, `maxGas: 2_000_000n`, `maxPriorityFeePerGas: 10_000_000_000n`, `maxTotalFee: 50_000_000_000_000_000n`, and `maxValidityWindowSeconds: 900`. On Moderato (`42431`), `maxPriorityFeePerGas` increases to `50_000_000_000n` and the other limits stay the same. @@ -319,7 +321,7 @@ const mppx = Mppx.create({ '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', ), feePayerPolicy: { - allowKeyAuthorization: false, + allowKeyAuthorization: true, maxPriorityFeePerGas: 50_000_000_000n, maxTotalFee: 100_000_000_000_000_000n, }, diff --git a/src/pages/sdk/typescript/server/Method.tempo.mdx b/src/pages/sdk/typescript/server/Method.tempo.mdx index 4987d319c..28493b29c 100644 --- a/src/pages/sdk/typescript/server/Method.tempo.mdx +++ b/src/pages/sdk/typescript/server/Method.tempo.mdx @@ -20,7 +20,7 @@ Use `tempo(...)` by default. Use `tempo.common(...)` only when you want to make Register [`tempo.subscription`](/sdk/typescript/server/Method.tempo.subscription) separately for recurring payments. :::warning[Legacy Sessions] -`tempo.session` is the current Sessions implementation. The previous contract-backed flow is Legacy Sessions, also called Sessions v1, and is available as `tempo.sessionLegacy` in `mppx` 0.8.15 and earlier. +`mppx` 0.13.0 removed the deprecated Legacy Sessions client exports. Close or settle Legacy channels before upgrading because current Sessions don't share their channel state. ::: ## Usage diff --git a/src/pages/sdk/typescript/server/Method.tempo.session.mdx b/src/pages/sdk/typescript/server/Method.tempo.session.mdx index c516d1765..dffaaf7bb 100644 --- a/src/pages/sdk/typescript/server/Method.tempo.session.mdx +++ b/src/pages/sdk/typescript/server/Method.tempo.session.mdx @@ -14,7 +14,7 @@ Creates a Tempo Sessions server method for voucher verification, channel account ::: :::warning[Legacy Sessions] -The previous contract-backed implementation is Legacy Sessions, also called Sessions v1, and is available as `tempo.sessionLegacy` in `mppx` 0.8.15 and earlier. Use Sessions for new integrations. +`mppx` 0.13.0 removed the deprecated Legacy Sessions client exports. Close or settle Legacy channels before upgrading because current Sessions don't share their channel state. ::: ## Usage @@ -60,63 +60,9 @@ const mppx = Mppx.create({ }) ``` -### With Legacy Sessions - -Use `tempo.sessionLegacy()` only for clients that still send Legacy Sessions Credentials. Pin `mppx` to 0.8.15 or earlier when maintaining a Legacy Sessions server. - -```ts -import { Mppx, Store, tempo } from 'mppx/server' -import { privateKeyToAccount } from 'viem/accounts' - -const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef') - -const mppx = Mppx.create({ - methods: [ - tempo.sessionLegacy({ - account, - currency: '0x20c0000000000000000000006a37DA5C996874BE', // OUSD on Tempo - store: Store.memory(), - }), - ], -}) -``` - ## Migrate from Legacy Sessions -Register `tempo.session()` beside `tempo.sessionLegacy()` during migration so existing clients keep working while new clients use Sessions. This server-side migration configuration requires `mppx` 0.8.15 or earlier. - -| Server methods | Compatible client methods | -|---|---| -| `tempo.session()` only | `tempo.session()` or `tempo()` | -| `tempo.sessionLegacy()` only | `tempo.sessionLegacy()` or `tempo.sessionLegacy.method()` | -| `tempo.session()` and `tempo.sessionLegacy()` | `tempo.session()` plus `tempo.sessionLegacy.method()` when clients may see both Challenge types | - -Current Sessions Challenges include `sessionProtocol: "v2"` and use TIP-1034 reserve channels. Legacy Sessions use the contract-backed Sessions v1 flow. Channel state is not portable between them, so close or settle Legacy channels with `tempo.sessionLegacy` and open new channels with `tempo.session`. - -```ts -import { Mppx, Store, tempo } from 'mppx/server' -import { privateKeyToAccount } from 'viem/accounts' - -const account = privateKeyToAccount('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef') - -const mppx = Mppx.create({ - methods: [ - tempo.session({ - account, - chainId: 4217, - currencies: ['0x20c0000000000000000000006a37DA5C996874BE'], // OUSD on Tempo - store: Store.memory(), - }), - tempo.sessionLegacy({ - account, - currency: '0x20c0000000000000000000006a37DA5C996874BE', // OUSD on Tempo - store: Store.memory(), - }), - ], -}) -``` - -Use `tempo.sessionLegacy()` only while clients still send Legacy Sessions Credentials. +Legacy Sessions used a contract-backed channel flow. Close or settle those channels with the older SDK before upgrading. Then register `tempo.session()` on the server and `tempo.session()` or `tempo()` on clients. New Sessions advertise `sessionProtocol: "v2"` and use TIP-1034 reserve channels. ### With same-route bootstrap diff --git a/src/pages/sdk/typescript/server/Mppx.toNodeListener.mdx b/src/pages/sdk/typescript/server/Mppx.toNodeListener.mdx index 91291323e..69d9b8b72 100644 --- a/src/pages/sdk/typescript/server/Mppx.toNodeListener.mdx +++ b/src/pages/sdk/typescript/server/Mppx.toNodeListener.mdx @@ -29,6 +29,7 @@ http.createServer(async (req, res) => { ## Behavior +- **On an oversized body:** Writes `413 Payload Too Large`. Request bodies default to a 10 MiB limit. - **On `402`:** Writes the Challenge response headers and body, then ends the connection. - **On `200`:** Sets the `Payment-Receipt` header; the caller writes the response body. @@ -49,6 +50,12 @@ type ReturnType = ( The payment handler function returned by calling an intent on the payment object. +### options (optional) + +- **Type:** `{ host?: string; maxBodySize?: number; protocol?: string }` + +Configures Node-to-Fetch request conversion. `maxBodySize` defaults to `10_485_760` bytes (10 MiB). + ```ts twoslash import * as http from 'node:http' import { Mppx, tempo } from 'mppx/server' diff --git a/src/pages/sdk/typescript/server/Request.toNodeListener.mdx b/src/pages/sdk/typescript/server/Request.toNodeListener.mdx index a2fd747f1..b5db8017d 100644 --- a/src/pages/sdk/typescript/server/Request.toNodeListener.mdx +++ b/src/pages/sdk/typescript/server/Request.toNodeListener.mdx @@ -11,6 +11,8 @@ import { Request } from 'mppx/server' const listener = Request.toNodeListener((request) => { const pathname = new URL(request.url).pathname return new Response(`Requested ${pathname}`) +}, { + maxBodySize: 1024 * 1024, }) http.createServer(listener).listen(3000) @@ -25,11 +27,23 @@ import type { IncomingMessage, ServerResponse } from 'node:http' import { Request } from 'mppx/server' function middleware(req: IncomingMessage, res: ServerResponse) { - const request = Request.fromNodeListener(req, res) + const request = Request.fromNodeListener(req, res, { maxBodySize: 1024 * 1024 }) // handle as a Fetch API Request } ``` +The adapter streams Node request bodies with backpressure and rejects bodies larger than `maxBodySize`. `Request.toNodeListener` returns `413 Payload Too Large` by default. When you call `Request.fromNodeListener` directly, await `Request.waitForBody(request)` before committing a successful response so late stream errors and size violations are observed. + +### `Request.waitForBody` + +Waits for a Node-backed Fetch request body to finish. It resolves immediately for other requests. + +```ts +const request = Request.fromNodeListener(req, res) +const response = await handler(request) +await Request.waitForBody(request) +``` + ## Parameters ### handler @@ -40,6 +54,6 @@ A Fetch API handler that receives a `Request` and returns a `Response`. ### options (optional) -- **Type:** `RequestListenerOptions` +- **Type:** `{ host?: string; maxBodySize?: number; onError?: (error: unknown) => void | Response | Promise; protocol?: string }` -Options forwarded to the underlying adapter, including an optional error handler. +Configures the trusted request origin, body limit, and error response. `maxBodySize` defaults to `10_485_760` bytes (10 MiB). diff --git a/src/pages/sdk/typescript/server/Transport.from.mdx b/src/pages/sdk/typescript/server/Transport.from.mdx index 3975eb81c..664026cac 100644 --- a/src/pages/sdk/typescript/server/Transport.from.mdx +++ b/src/pages/sdk/typescript/server/Transport.from.mdx @@ -9,7 +9,7 @@ import { Challenge, Credential, Receipt } from 'mppx' import { Transport } from 'mppx/server' const http = Transport.from({ - name: 'http', + credentialHeaders: ['Authorization'], getCredential(request) { const header = request.headers.get('Authorization') @@ -19,6 +19,8 @@ const http = Transport.from({ return Credential.deserialize(payment) }, + name: 'http', + respondChallenge({ challenge, error }) { const headers: Record = { 'WWW-Authenticate': Challenge.serialize(challenge), @@ -49,17 +51,41 @@ const http = Transport.from({ ## Return type ```ts -type ReturnType = Transport +type ReturnType = Transport ``` ## Parameters +### bindCredential (optional) + +- **Type:** `(options: { challenge: Challenge; credential: Credential; input: Input }) => Credential | Promise` + +Rebinds a transport-native Credential to the normalized route Challenge. Use this when the wire format doesn't carry an MPP Challenge directly. + +### captureRequest (optional) + +- **Type:** `(input: Input) => CapturedRequest | Promise` + +Captures an immutable request snapshot for Challenge binding and later verification. + +### credentialHeaders (optional) + +- **Type:** `readonly string[]` + +HTTP header names from which this transport can read Credentials. Composed methods use this list to keep Credentials scoped to their matching transport. + ### getCredential - **Type:** `(input: Input) => Credential | null` Extracts Credential from the transport input. Returns `null` if no Credential was provided, or throws if malformed. +### matchCredential (optional) + +- **Type:** `(options: { input: Input; request: Record }) => boolean | Promise` + +Returns whether a transport-native Credential targets one configured payment request. Use this when several configured offers share the same wire method and intent. + ### name - **Type:** `string` @@ -74,6 +100,12 @@ Creates a transport response for a payment Challenge. ### respondReceipt -- **Type:** `(options: { challengeId: string; receipt: Receipt; response: ReceiptOutput }) => ReceiptOutput` +- **Type:** `(options: { challengeId: string; credential: Credential; envelope?: VerifiedChallengeEnvelope; input: Input; receipt: Receipt; response: ReceiptResponse }) => ReceiptOutput` Attaches a Receipt to a successful response. + +### supportsStreamingReceipts (optional) + +- **Type:** `boolean` + +Marks transports whose Receipt wrapper accepts route-produced async iterables. Framework adapters use this to preserve streaming payment accounting. diff --git a/src/pages/sdk/typescript/server/Ws.serve.mdx b/src/pages/sdk/typescript/server/Ws.serve.mdx index 86b2be577..d91d2bd6c 100644 --- a/src/pages/sdk/typescript/server/Ws.serve.mdx +++ b/src/pages/sdk/typescript/server/Ws.serve.mdx @@ -81,6 +81,20 @@ Expected per-tick amount. When set, Credentials with mismatched amounts are reje Async iterable that produces application messages. When passed as a function, receives a `SessionController` with a `charge(amount?: bigint)` method for requesting payment before yielding each value. Omit `amount` to use the Challenge tick cost, or pass a base-unit amount for dynamic pricing. Each yielded string is sent to the client as an application message frame. +### maxBufferedAmount (optional) + +- **Type:** `number` +- **Default:** `1_048_576` + +Maximum queued outbound WebSocket bytes. The helper closes the socket when its buffered amount exceeds this limit. + +### maxIncomingMessageBytes (optional) + +- **Type:** `number` +- **Default:** `65_536` + +Maximum accepted inbound WebSocket frame size. The helper returns a payment error and closes the socket with code `1009` when a frame exceeds this limit. + ### pollIntervalMs (optional) - **Type:** `number` @@ -230,12 +244,13 @@ type Message = ```ts type Socket = { - close(code?: number, reason?: string): unknown - send(data: string): unknown addEventListener?: (type: string, listener: (event: any) => void) => unknown - removeEventListener?: (type: string, listener: (event: any) => void) => unknown - on?: (type: string, listener: (...args: any[]) => void) => unknown + readonly bufferedAmount?: number + close(code?: number, reason?: string): unknown off?: (type: string, listener: (...args: any[]) => void) => unknown + on?: (type: string, listener: (...args: any[]) => void) => unknown + removeEventListener?: (type: string, listener: (event: any) => void) => unknown + send(data: string): unknown } ``` @@ -244,7 +259,7 @@ type Socket = { ```ts type SessionRouteResult = | { status: 402; challenge: Response } - | { status: 200; withReceipt(response?: Response): Response } + | { status: 200; withReceipt(response?: Response): Response | Promise } type SessionRoute = (request: Request) => Promise ```