diff --git a/.mppx-docs-sync b/.mppx-docs-sync index 3eead9bd..7c3cde3f 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.0.0-main-20260805032439 -mppx_sha=44414785ff5efb42f50fd89f06aae72e5e9af3de +mppx_version=0.0.0-main-20260805221648 +mppx_sha=cd71f9e5c12bdae75d3b1ea8dd0a677c2d0af7e7 diff --git a/package.json b/package.json index 3c3d0b4f..233d7c66 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "hono": "^4.12.27", "lottie-web": "^5.13.0", "mermaid": "^11.15.0", - "mppx": "0.0.0-main-20260805032439", + "mppx": "0.0.0-main-20260805221648", "nuqs": "2.9.1", "react": "^19", "react-dom": "^19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10a82511..1f5f59b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: ^11.15.0 version: 11.15.0 mppx: - specifier: 0.0.0-main-20260805032439 - version: 0.0.0-main-20260805032439(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.27)(typescript@6.0.3)(viem@2.54.0(typescript@6.0.3)(zod@4.4.3)) + specifier: 0.0.0-main-20260805221648 + version: 0.0.0-main-20260805221648(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.27)(typescript@6.0.3)(viem@2.54.0(typescript@6.0.3)(zod@4.4.3)) nuqs: specifier: 2.9.1 version: 2.9.1(react@19.2.7) @@ -4092,8 +4092,8 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mppx@0.0.0-main-20260805032439: - resolution: {integrity: sha512-LfayXKwjNvqoaD9Awy/aOz0JxA5ykayVd7Om8gBw5b86p+5vRwmc2OP+wB+35tweIiF7xECDiVZ+AFUzHPnX8w==} + mppx@0.0.0-main-20260805221648: + resolution: {integrity: sha512-bk4wXzNMkUuYKtfGccsE03Dt7YUuCmPrSQFr83GjzFZwfOX+9r1c+uINnihIhEnrFwWTlGn9htLIQJfjUWwW6Q==} hasBin: true peerDependencies: '@modelcontextprotocol/sdk': '>=1.25.0' @@ -9697,7 +9697,7 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - mppx@0.0.0-main-20260805032439(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.27)(typescript@6.0.3)(viem@2.54.0(typescript@6.0.3)(zod@4.4.3)): + mppx@0.0.0-main-20260805221648(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.27)(typescript@6.0.3)(viem@2.54.0(typescript@6.0.3)(zod@4.4.3)): dependencies: '@stripe/stripe-js': 9.12.0 eventsource-parser: 3.1.0 diff --git a/src/pages.gen.ts b/src/pages.gen.ts index 80b63290..6740a208 100644 --- a/src/pages.gen.ts +++ b/src/pages.gen.ts @@ -157,6 +157,7 @@ type Page = | { path: '/sdk/typescript/core/Receipt.from'; render: 'static' } | { path: '/sdk/typescript/core/Receipt.fromResponse'; render: 'static' } | { path: '/sdk/typescript/core/Receipt.serialize'; render: 'static' } + | { path: '/sdk/typescript/core/Store.tryClaim'; render: 'static' } | { path: '/sdk/typescript/html/custom'; render: 'static' } | { path: '/sdk/typescript'; render: 'static' } | { path: '/sdk/typescript/middlewares/elysia'; render: 'static' } diff --git a/src/pages/advanced/identity.mdx b/src/pages/advanced/identity.mdx index ab75db30..f783dd32 100644 --- a/src/pages/advanced/identity.mdx +++ b/src/pages/advanced/identity.mdx @@ -84,9 +84,8 @@ console.log(response.status) Configure the verifier on the MPP server. The resolver receives the signed directory origin and key ID. Apply your trust policy before you return a key. ```ts twoslash [server.ts] -import * as Attestation from 'mppx/attestation' import * as WebBotAuth from 'mppx/attestation/web-bot-auth' -import { Mppx, tempo } from 'mppx/server' +import { Mppx, Store, tempo } from 'mppx/server' declare const botPublicKey: CryptoKey @@ -107,7 +106,7 @@ const payment = Mppx.create({ return botPublicKey }, maxAge: 60, - nonceStore: Attestation.NonceStore.memory(), + nonceStore: Store.memory(), }), }, // [!code hl:end] @@ -157,9 +156,8 @@ console.log(response.status) On the server, resolve `keyId` only from agent providers you trust. The verifier checks the signature, request authority and path, lifetime, intent tag, and nonce. ```ts twoslash [server.ts] -import * as Attestation from 'mppx/attestation' import * as Tap from 'mppx/attestation/tap' -import { Mppx, tempo } from 'mppx/server' +import { Mppx, Store, tempo } from 'mppx/server' declare const trustedAgentKeys: ReadonlyMap @@ -170,7 +168,7 @@ const payment = Mppx.create({ keyResolver({ keyId }) { return trustedAgentKeys.get(keyId) }, - nonceStore: Attestation.NonceStore.memory(), + nonceStore: Store.memory(), }), }, // [!code hl:end] @@ -210,7 +208,7 @@ import { Mppx, tempo } from 'mppx/server' declare const trustedAgentKeys: ReadonlyMap -const nonceStore = Attestation.NonceStore.memory() +const nonceStore = Attestation.Store.memory() const tap = Tap.Server.verifier({ keyResolver({ keyId }) { return trustedAgentKeys.get(keyId) @@ -240,23 +238,25 @@ Don't pass the same verifier to `Mppx.create` in this pattern. Verification cons ### Store nonces -`Attestation.NonceStore.memory()` is limited to one long-lived server process. In a multi-instance deployment, provide a shared store whose `consume` operation atomically inserts a nonce only when absent and retains it until `expires`. +Attestation verifiers accept the core `Store.AtomicStore`. `Store.memory()` is limited to one long-lived server process. In a multi-instance deployment, provide a shared atomic store so every instance claims nonces through their expiration time. ```ts [nonce-store.ts] -import type { NonceStore } from 'mppx/attestation' - -export const nonceStore: NonceStore.Store = { - async consume(key, expires) { - const inserted = await nonceDatabase.insertIfAbsent({ - expires, - key, - }) - return !inserted - }, +import type { Store } from 'mppx' + +declare const nonceDatabase: { + insertIfAbsent(value: { expires: number; key: string }): Promise } +declare const sharedStore: Store.AtomicStore + +export const nonceStore = { + ...sharedStore, + async tryClaim(key: string, expires: number) { + return nonceDatabase.insertIfAbsent({ expires, key }) + }, +} satisfies Store.AtomicStore ``` -Adapt the call to your storage client. `consume` returns `true` when the nonce was already present and unexpired. +Adapt `tryClaim` to your storage client's atomic insert-if-absent operation. It returns `true` when it records a new claim and `false` when an unexpired claim already exists. If your `AtomicStore` omits this optimized method, `mppx` falls back to its atomic `update` operation. See [`Store.tryClaim`](/sdk/typescript/core/Store.tryClaim). ## MPP Credential identity diff --git a/src/pages/advanced/payment-hooks.mdx b/src/pages/advanced/payment-hooks.mdx index 103a7dd0..d6d550c0 100644 --- a/src/pages/advanced/payment-hooks.mdx +++ b/src/pages/advanced/payment-hooks.mdx @@ -88,6 +88,31 @@ payment.onPaymentFailed(({ error, method, submittedChallenge }) => { // [!code h }) ``` +### Scope success hooks to a method + +Pass `onPaymentSuccess` to a method constructor when the side effect belongs only to that payment method and intent. The hook receives the method-specific request, its Receipt, and the HTTP input when available. + +```ts +import { Mppx, tempo } from 'mppx/server' + +const payment = Mppx.create({ + methods: [ + tempo.charge({ + async onPaymentSuccess({ input, receipt, request }) { + await recordCharge({ + amount: request.amount, + path: input ? new URL(input.url).pathname : undefined, + reference: receipt.reference, + }) + }, + }), + tempo.session(), + ], +}) +``` + +`mppx` registers this as a filtered `payment.success` listener. It runs only when both the method name and intent match. The server awaits it inline and ignores thrown errors, matching instance-level server hook behavior. `input` is absent for standalone `broadcastCredential` and `verifyCredential` calls. + ## Client hooks Register client hooks on the object returned by `Mppx.create` from `mppx/client`. diff --git a/src/pages/blog/multi-method-discovery.mdx b/src/pages/blog/multi-method-discovery.mdx index 70d079fd..7feedc17 100644 --- a/src/pages/blog/multi-method-discovery.mdx +++ b/src/pages/blog/multi-method-discovery.mdx @@ -102,6 +102,8 @@ const mppx = Mppx.create({ }), stripe.charge({ client: stripeClient, + currency: 'usd', + decimals: 2, networkId: 'internal', paymentMethodTypes: ['card'], }), diff --git a/src/pages/guides/multiple-payment-methods.mdx b/src/pages/guides/multiple-payment-methods.mdx index 2ee7327f..a84adf88 100644 --- a/src/pages/guides/multiple-payment-methods.mdx +++ b/src/pages/guides/multiple-payment-methods.mdx @@ -43,7 +43,7 @@ WWW-Authenticate: Payment method="stripe", intent="charge", ... WWW-Authenticate: Payment method="lightning", intent="charge", ... ``` -The server verifies whichever Credential it receives. Your route handler stays the same regardless of which method the client chose. +The server verifies whichever Credential it receives. Intent shorthand such as `mppx.charge(options)` implicitly composes every registered method with that intent when they share compatible request units. Compose methods explicitly when one method needs different options—Lightning uses satoshis, while this guide prices Tempo and Stripe in US dollars. ## Server setup @@ -69,21 +69,22 @@ Register all three methods in a single `Mppx.create` call. Each method has its o ```ts [server.ts] import Stripe from 'stripe' -import { Mppx, tempo, stripe } from 'mppx/server' +import { Mppx, stripe, tempo } from 'mppx/server' import { spark } from '@buildonspark/lightning-mpp-sdk/server' const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ - secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo.charge({ - testnet: true, currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + testnet: true, }), stripe.charge({ client: stripeClient, + currency: 'usd', + decimals: 2, networkId: 'internal', paymentMethodTypes: ['card'], }), @@ -91,31 +92,35 @@ const mppx = Mppx.create({ mnemonic: process.env.MNEMONIC!, }), ], + secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) ``` ### Create a payment-gated route -The route handler is identical to a single-method setup. `mppx.charge` advertises all registered methods in the Challenge and verifies whichever Credential the client presents. +Compose the methods explicitly so each offer receives an equivalent price in its native unit. Resolve the Lightning amount from a trusted BTC/USD price feed, then pass the dollar amount to Tempo and Stripe and the converted satoshi amount to Lightning. ```ts [server.ts] import crypto from 'crypto' import Stripe from 'stripe' -import { Mppx, tempo, stripe } from 'mppx/server' +import { Mppx, stripe, tempo } from 'mppx/server' import { spark } from '@buildonspark/lightning-mpp-sdk/server' +declare function quoteUsdInSats(usdAmount: string): Promise + const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ - secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo.charge({ - testnet: true, currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + testnet: true, }), stripe.charge({ client: stripeClient, + currency: 'usd', + decimals: 2, networkId: 'internal', paymentMethodTypes: ['card'], }), @@ -123,17 +128,22 @@ const mppx = Mppx.create({ mnemonic: process.env.MNEMONIC!, }), ], + secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) // [!code focus:start] +const description = 'Premium API access' +const usdAmount = '0.01' +const lightningSats = await quoteUsdInSats(usdAmount) +const charge = mppx.compose( + ['lightning/charge', { amount: lightningSats, description }], + ['stripe/charge', { amount: usdAmount, description }], + ['tempo/charge', { amount: usdAmount, description }], +) + Bun.serve({ async fetch(request) { - const result = await mppx.charge({ - amount: '0.01', - currency: 'usd', - decimals: 2, - description: 'Premium API access', - })(request) + const result = await charge(request) if (result.status === 402) return result.challenge @@ -162,7 +172,7 @@ $ npx mppx http://localhost:3000 ## Framework examples -The `Mppx.create` configuration is the same across frameworks—only the route handler syntax changes. +The `Mppx.create` configuration is the same across frameworks—only the route handler syntax changes. These examples resolve the Lightning quote at startup for brevity; refresh it before it becomes stale according to your pricing policy. ### Hono @@ -170,22 +180,25 @@ The `Mppx.create` configuration is the same across frameworks—only the route h import crypto from 'crypto' import { Hono } from 'hono' import Stripe from 'stripe' -import { Mppx, tempo, stripe } from 'mppx/hono' +import { Mppx, stripe, tempo } from 'mppx/hono' import { spark } from '@buildonspark/lightning-mpp-sdk/server' +declare function quoteUsdInSats(usdAmount: string): Promise + const app = new Hono() const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ - secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo.charge({ - testnet: true, currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + testnet: true, }), stripe.charge({ client: stripeClient, + currency: 'usd', + decimals: 2, networkId: 'internal', paymentMethodTypes: ['card'], }), @@ -193,11 +206,21 @@ const mppx = Mppx.create({ mnemonic: process.env.MNEMONIC!, }), ], + secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) +const description = 'Premium API access' +const usdAmount = '0.01' +const lightningSats = await quoteUsdInSats(usdAmount) +const charge = mppx.compose( + ['lightning/charge', { amount: lightningSats, description }], + ['stripe/charge', { amount: usdAmount, description }], + ['tempo/charge', { amount: usdAmount, description }], +) + app.get( '/api/resource', - mppx.charge({ amount: '0.01', currency: 'usd', decimals: 2, description: 'Premium API access' }), + charge, async (c) => c.json({ message: 'Paid content' }), ) ``` @@ -208,22 +231,25 @@ app.get( import crypto from 'crypto' import express from 'express' import Stripe from 'stripe' -import { Mppx, tempo, stripe } from 'mppx/express' +import { Mppx, stripe, tempo } from 'mppx/express' import { spark } from '@buildonspark/lightning-mpp-sdk/server' +declare function quoteUsdInSats(usdAmount: string): Promise + const app = express() const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ - secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo.charge({ - testnet: true, currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + testnet: true, }), stripe.charge({ client: stripeClient, + currency: 'usd', + decimals: 2, networkId: 'internal', paymentMethodTypes: ['card'], }), @@ -231,11 +257,21 @@ const mppx = Mppx.create({ mnemonic: process.env.MNEMONIC!, }), ], + secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) +const description = 'Premium API access' +const usdAmount = '0.01' +const lightningSats = await quoteUsdInSats(usdAmount) +const charge = mppx.compose( + ['lightning/charge', { amount: lightningSats, description }], + ['stripe/charge', { amount: usdAmount, description }], + ['tempo/charge', { amount: usdAmount, description }], +) + app.get( '/api/resource', - mppx.charge({ amount: '0.01', currency: 'usd', decimals: 2, description: 'Premium API access' }), + charge, async (req, res) => res.json({ message: 'Paid content' }), ) ``` @@ -245,21 +281,24 @@ app.get( ```ts [app/api/resource/route.ts] import crypto from 'crypto' import Stripe from 'stripe' -import { Mppx, tempo, stripe } from 'mppx/nextjs' +import { Mppx, stripe, tempo } from 'mppx/nextjs' import { spark } from '@buildonspark/lightning-mpp-sdk/server' +declare function quoteUsdInSats(usdAmount: string): Promise + const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!) const mppx = Mppx.create({ - secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), methods: [ tempo.charge({ - testnet: true, currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + testnet: true, }), stripe.charge({ client: stripeClient, + currency: 'usd', + decimals: 2, networkId: 'internal', paymentMethodTypes: ['card'], }), @@ -267,11 +306,21 @@ const mppx = Mppx.create({ mnemonic: process.env.MNEMONIC!, }), ], + secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'), }) -export const GET = - mppx.charge({ amount: '0.01', currency: 'usd', decimals: 2, description: 'Premium API access' }) - (async () => Response.json({ message: 'Paid content' })) +const description = 'Premium API access' +const usdAmount = '0.01' +const lightningSats = await quoteUsdInSats(usdAmount) +const charge = mppx.compose( + ['lightning/charge', { amount: lightningSats, description }], + ['stripe/charge', { amount: usdAmount, description }], + ['tempo/charge', { amount: usdAmount, description }], +) + +export const GET = charge( + async () => Response.json({ message: 'Paid content' }), +) ``` ## Method-specific configuration diff --git a/src/pages/guides/use-mpp-with-x402.mdx b/src/pages/guides/use-mpp-with-x402.mdx index 87253d1b..ecc5c1cb 100644 --- a/src/pages/guides/use-mpp-with-x402.mdx +++ b/src/pages/guides/use-mpp-with-x402.mdx @@ -396,6 +396,8 @@ const mppx = Mppx.create({ // [!code hl:start] stripe.charge({ client: stripeClient, + currency: 'usd', + decimals: 2, networkId: 'internal', paymentMethodTypes: ['card'], }), diff --git a/src/pages/sdk/typescript/core/Challenge.fromResponseList.mdx b/src/pages/sdk/typescript/core/Challenge.fromResponseList.mdx index 6e7c3b41..64e83b7a 100644 --- a/src/pages/sdk/typescript/core/Challenge.fromResponseList.mdx +++ b/src/pages/sdk/typescript/core/Challenge.fromResponseList.mdx @@ -13,7 +13,7 @@ if (response.status === 402) { } ``` -The returned array preserves the order of the Payment Challenges in the `WWW-Authenticate` header. +The returned array preserves the order of the Payment Challenges in the `WWW-Authenticate` header. Quoted parameter values are parsed as data, including values that contain the `Payment` scheme name. ### With method type narrowing diff --git a/src/pages/sdk/typescript/core/Method.toServer.mdx b/src/pages/sdk/typescript/core/Method.toServer.mdx index c468d111..9a6c7efe 100644 --- a/src/pages/sdk/typescript/core/Method.toServer.mdx +++ b/src/pages/sdk/typescript/core/Method.toServer.mdx @@ -111,6 +111,12 @@ Returns whether this method's configured offer is available when an HTTP handler Default request parameters merged into every Challenge issued for this method. +#### onPaymentSuccess (optional) + +- **Type:** `Method.OnPaymentSuccessFn` + +Runs after this method completes successfully. The hook receives a deeply immutable `request`, the `receipt`, and the HTTP `input` when available. `Mppx.create` scopes it to the method's name and intent, awaits it inline, and ignores thrown errors. + #### request (optional) - **Type:** `(options: { credential?: Credential; request: request }) => request` diff --git a/src/pages/sdk/typescript/core/Store.tryClaim.mdx b/src/pages/sdk/typescript/core/Store.tryClaim.mdx new file mode 100644 index 00000000..db648d02 --- /dev/null +++ b/src/pages/sdk/typescript/core/Store.tryClaim.mdx @@ -0,0 +1,70 @@ +--- +description: "Prevent duplicate payments and request replays with atomic, expiring claims." +imageDescription: "Block replayed payment credentials" +--- + +# `Store.tryClaim` [Claim replay keys atomically] + +Records the first use of a replay key until its expiration time. + +## Usage + +```ts twoslash +import { Store } from 'mppx' + +const store = Store.memory() +const expires = Date.now() + 60_000 + +const claimed = await Store.tryClaim(store, 'request:abc123', expires) +console.log(claimed) +// @log: true + +const replayed = await Store.tryClaim(store, 'request:abc123', expires) +console.log(replayed) +// @log: false +``` + +`Store.tryClaim` uses the store's optional optimized `tryClaim` operation when present. Otherwise, it falls back to `AtomicStore.update`. Expired replay markers can be claimed again; legacy non-marker values remain claimed. + +### Support replay claims in a custom store + +`AtomicStore` accepts an optional `tryClaim` fast path. The fallback stores a `ReplayMarker`, so include that type in a custom store's item map when you don't provide the fast path. + +```ts +type TryClaim = < + key extends keyof itemMap & string, +>(key: key, expires: number) => boolean | Promise + +type ReplayMarker = { + expires: number + type: 'mppx:replay' +} +``` + +## Return type + +```ts +type ReturnType = boolean | Promise +``` + +Returns `true` when this call records the key and `false` when an unexpired claim already exists. + +## Parameters + +### expires + +- **Type:** `number` + +Unix timestamp in milliseconds when the replay claim expires. + +### key + +- **Type:** `string` + +Store key to claim. Typed stores constrain this value to their item-map keys. + +### store + +- **Type:** `Store.AtomicStore` + +Atomic store used to persist the replay marker. Implement `store.tryClaim` as a single insert-if-absent-with-expiry operation when your backend supports it. diff --git a/src/pages/sdk/typescript/server/Method.evm.charge.mdx b/src/pages/sdk/typescript/server/Method.evm.charge.mdx index efaa19e4..484cf30b 100644 --- a/src/pages/sdk/typescript/server/Method.evm.charge.mdx +++ b/src/pages/sdk/typescript/server/Method.evm.charge.mdx @@ -98,6 +98,12 @@ const method = evm.charge({ Token decimal places. Required for custom currency addresses and inferred for known assets. +### onPaymentSuccess (optional) + +- **Type:** `Method.OnPaymentSuccessFn` + +Runs after this EVM charge succeeds. The hook receives the normalized request, its Receipt, and the HTTP input when available. Errors don't change payment handling. + ### recipient - **Type:** `Address` diff --git a/src/pages/sdk/typescript/server/Method.tempo.charge.mdx b/src/pages/sdk/typescript/server/Method.tempo.charge.mdx index d3060d5e..728bbbd4 100644 --- a/src/pages/sdk/typescript/server/Method.tempo.charge.mdx +++ b/src/pages/sdk/typescript/server/Method.tempo.charge.mdx @@ -233,6 +233,12 @@ Function that returns a viem client for the given chain ID. Overrides the defaul On-chain memo for the transaction. +### onPaymentSuccess (optional) + +- **Type:** `Method.OnPaymentSuccessFn` + +Runs after this Tempo charge succeeds. The hook receives the normalized request, its Receipt, and the HTTP input when available. Errors don't change payment handling. + ### relay (optional) - **Type:** `{ apiBaseUrl?: string; apiKey: string; fetch?: typeof globalThis.fetch }` @@ -243,11 +249,13 @@ Delegates Tempo charge Credential validation and finalization to a compatible MP - **Type:** `Store.AtomicStore` -Pass a store when you want replay protection for charge Credentials. A `Store` provides async key-value operations (`get`, `put`, `delete`). An `AtomicStore` extends `Store` with an atomic `update(key, fn)` method for safe concurrent replay checks. +Pass a store when you want replay protection for charge Credentials. A `Store` provides async key-value operations (`get`, `put`, `delete`). An `AtomicStore` extends `Store` with an atomic `update(key, fn)` method and an optional optimized `tryClaim(key, expires)` operation. For non-zero charges, `mppx` falls back to an in-memory store when you omit this parameter. For zero-dollar proof auth, replay prevention is disabled unless you pass a store. -Use `Store.memory()` for local development, tests, or a single long-lived server process. For multi-instance deployments, use `Store.redis()`, `Store.upstash()`, or `Store.cloudflare()`. All built-in factories return `AtomicStore` — for custom backends, provide an `update` function alongside `get`, `put`, and `delete`. +`mppx` claims transaction hashes and proof Challenge IDs through the Challenge expiration time. It calls [`Store.tryClaim`](/sdk/typescript/core/Store.tryClaim), which uses the store's optimized operation when present and otherwise falls back to `update`. + +Use `Store.memory()` for local development, tests, or a single long-lived server process. For multi-instance deployments, use `Store.redis()`, `Store.upstash()`, or `Store.cloudflare()`. All built-in factories return `AtomicStore`—for custom backends, provide an `update` function alongside `get`, `put`, and `delete`. ### testnet (optional) diff --git a/src/pages/sdk/typescript/server/Method.tempo.mdx b/src/pages/sdk/typescript/server/Method.tempo.mdx index 34ee7a23..c027282e 100644 --- a/src/pages/sdk/typescript/server/Method.tempo.mdx +++ b/src/pages/sdk/typescript/server/Method.tempo.mdx @@ -125,6 +125,12 @@ Account or URL for sponsoring transaction fees. Pass a viem `Account` to co-sign Function that returns a viem client for the given chain ID. Overrides the default RPC configuration. +### onPaymentSuccess (optional) + +- **Type:** `Method.OnPaymentSuccessFn & Method.OnPaymentSuccessFn` + +Runs after a configured Tempo charge or Sessions result succeeds. The hook receives the normalized request, its Receipt, and the HTTP input when available. Errors don't change payment handling. + ### recipient (optional) - **Type:** `Address` diff --git a/src/pages/sdk/typescript/server/Method.tempo.session.mdx b/src/pages/sdk/typescript/server/Method.tempo.session.mdx index ffb67724..3e0fc380 100644 --- a/src/pages/sdk/typescript/server/Method.tempo.session.mdx +++ b/src/pages/sdk/typescript/server/Method.tempo.session.mdx @@ -250,6 +250,12 @@ Function that returns a viem client for the given chain ID. Minimum voucher delta to accept as a numeric string. Rejects vouchers where the increment over the previous highest voucher is below this threshold. +### onPaymentSuccess (optional) + +- **Type:** `Method.OnPaymentSuccessFn` + +Runs after this Sessions method returns a successful result. The hook receives the normalized request, its Receipt, and the HTTP input when available. Errors don't change payment handling. + ### operator (optional) - **Type:** `Address` diff --git a/src/pages/sdk/typescript/server/Method.tempo.subscription.mdx b/src/pages/sdk/typescript/server/Method.tempo.subscription.mdx index 2c3b18d5..c93ea308 100644 --- a/src/pages/sdk/typescript/server/Method.tempo.subscription.mdx +++ b/src/pages/sdk/typescript/server/Method.tempo.subscription.mdx @@ -233,6 +233,12 @@ Function that returns a viem client for the given Tempo chain ID. Callbacks that run after activation or renewal commits. +### onPaymentSuccess (optional) + +- **Type:** `Method.OnPaymentSuccessFn` + +Runs after this subscription method returns a successful result. The hook receives the normalized request, its Receipt, and the HTTP input when available. Errors don't change payment handling. + ### periodCount (optional) - **Type:** `string` diff --git a/src/pages/sdk/typescript/server/Mppx.compose.mdx b/src/pages/sdk/typescript/server/Mppx.compose.mdx index 98589a88..ce186676 100644 --- a/src/pages/sdk/typescript/server/Mppx.compose.mdx +++ b/src/pages/sdk/typescript/server/Mppx.compose.mdx @@ -54,6 +54,7 @@ const paid = Mppx.compose( ## Behavior - **No Credential present:** Calls all handlers and merges their `402` Challenges into a single response with multiple `WWW-Authenticate` headers. +- **Intent shorthand:** When multiple registered methods share an intent, `mppx.charge(options)` and other intent functions implicitly compose every matching method. Use explicit composition when offers need different options. - **Offer policies:** Calls each method's `canOffer` hook, then the instance-level [`selectOffers`](/sdk/typescript/server/Mppx.create#selectoffers-optional) hook, before generating Payment auth, x402, or HTML offers. Static compositions apply `canOffer`; direct method handlers don't. - **`Accept-Payment` present:** Ranks and filters the merged Challenges by the client's supported `method/intent` entries. Entries with `q=0` are excluded. If the header is invalid or filters out every Challenge, all Challenges are returned. - **Credential present:** Dispatches to the handler matching the Credential's `method` and `intent` without re-running offer policies. diff --git a/src/pages/sdk/typescript/server/Mppx.create.mdx b/src/pages/sdk/typescript/server/Mppx.create.mdx index 3ead74c9..195ac79b 100644 --- a/src/pages/sdk/typescript/server/Mppx.create.mdx +++ b/src/pages/sdk/typescript/server/Mppx.create.mdx @@ -12,6 +12,38 @@ const payment = Mppx.create({ }) ``` +### With multiple methods for one intent + +Register multiple methods with the same intent, then use the intent shorthand. `mppx.charge()` configures every matching charge method and implicitly composes them into one handler. + +```ts +import Stripe from 'stripe' +import { Mppx, stripe, tempo } from 'mppx/server' + +const payment = Mppx.create({ + methods: [ + stripe.charge({ + client: new Stripe(process.env.STRIPE_SECRET_KEY!), + currency: 'usd', + decimals: 2, + networkId: 'internal', + paymentMethodTypes: ['card'], + }), + tempo.charge({ + currency: '0x20c0000000000000000000000000000000000000', // pathUSD on Tempo + decimals: 6, + recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + }), + ], +}) + +const handler = payment.charge({ + amount: '1', +}) +``` + +The shorthand applies `canOffer` and `selectOffers`, just like `payment.compose()`. Use [`Mppx.compose`](/sdk/typescript/server/Mppx.compose) when methods need different request options or one method needs multiple offers. + ### Select offers per request Use `selectOffers` to filter composed HTTP offers before the server issues Challenges. The hook receives normalized, immutable offer snapshots and a clone of the incoming request. @@ -92,7 +124,7 @@ import type { Mppx, Transport } from 'mppx/server' type ReturnType = Mppx<[Method.Server], Transport.Http> ``` -The returned object includes the method's intent functions (for example, `charge`), `broadcastCredential`, `challenge`, `compose`, payment hooks, `validateCredential`, and `verifyCredential`. +The returned object includes intent functions (for example, `charge`), `broadcastCredential`, `challenge`, `compose`, payment hooks, `validateCredential`, and `verifyCredential`. An intent function calls its only matching method directly or implicitly composes every matching method. ### Validate and broadcast Credentials diff --git a/vocs.config.ts b/vocs.config.ts index 8da2b7df..b03eb011 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -963,6 +963,16 @@ export default defineConfig({ }, ], }, + { + text: "Store", + collapsed: true, + items: [ + { + text: ".tryClaim", + link: "/sdk/typescript/core/Store.tryClaim", + }, + ], + }, ], }, {