Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .mppx-docs-sync
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 21 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src/pages/guides/use-mpp-with-x402.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove stale standard-client claims after changing the default

With the new routeBinding: 'required' default, the preceding Express example receives automatically derived route metadata and therefore rejects standard x402 Credentials that lack the mppx extension. However, line 196 still says standard clients don't need that extension, and line 219 still says the default compares the echoed resource URL. This can send users into a repeated 402 flow; either set routeBinding: 'resource' in the primary example or update both stale statements to match the secure default.

AGENTS.md reference: AGENTS.md:L65-L67

Useful? React with 👍 / 👎.


```ts twoslash [server.ts]
import { evm } from 'mppx/server'
Expand All @@ -210,7 +210,7 @@ const method = evm.charge({
// [!code hl:start]
x402: {
facilitator: 'https://x402.org/facilitator',
routeBinding: 'required',
routeBinding: 'resource',
},
// [!code hl:end]
})
Expand Down
4 changes: 2 additions & 2 deletions src/pages/payment-methods/evm/charge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 7 additions & 1 deletion src/pages/payment-methods/stripe/charge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
Expand All @@ -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({
Expand Down Expand Up @@ -130,6 +135,7 @@ const payments = stripe.create({
hostedFeePayer: true,
livemode: true,
networkId: process.env.STRIPE_PROFILE_ID!,
store,
})
```

Expand Down
6 changes: 4 additions & 2 deletions src/pages/payment-methods/stripe/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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.

Expand Down
140 changes: 7 additions & 133 deletions src/pages/payment-methods/tempo/session.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

<Tabs stateKey="account-source">
<Tab title="Accounts SDK">

```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.

</Tab>
<Tab title="viem">

```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')
```

</Tab>
</Tabs>
- 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

Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading