Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.0.0-main-20260805032439
mppx_sha=44414785ff5efb42f50fd89f06aae72e5e9af3de
mppx_version=0.0.0-main-20260805221648
mppx_sha=cd71f9e5c12bdae75d3b1ea8dd0a677c2d0af7e7
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

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

1 change: 1 addition & 0 deletions src/pages.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down
38 changes: 19 additions & 19 deletions src/pages/advanced/identity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -107,7 +106,7 @@ const payment = Mppx.create({
return botPublicKey
},
maxAge: 60,
nonceStore: Attestation.NonceStore.memory(),
nonceStore: Store.memory(),
}),
},
// [!code hl:end]
Expand Down Expand Up @@ -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<string, CryptoKey>

Expand All @@ -170,7 +168,7 @@ const payment = Mppx.create({
keyResolver({ keyId }) {
return trustedAgentKeys.get(keyId)
},
nonceStore: Attestation.NonceStore.memory(),
nonceStore: Store.memory(),
}),
},
// [!code hl:end]
Expand Down Expand Up @@ -210,7 +208,7 @@ import { Mppx, tempo } from 'mppx/server'

declare const trustedAgentKeys: ReadonlyMap<string, CryptoKey>

const nonceStore = Attestation.NonceStore.memory()
const nonceStore = Attestation.Store.memory()
const tap = Tap.Server.verifier({
keyResolver({ keyId }) {
return trustedAgentKeys.get(keyId)
Expand Down Expand Up @@ -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<boolean>
}
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

Expand Down
25 changes: 25 additions & 0 deletions src/pages/advanced/payment-hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 2 additions & 0 deletions src/pages/blog/multi-method-discovery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ const mppx = Mppx.create({
}),
stripe.charge({
client: stripeClient,
currency: 'usd',
decimals: 2,
networkId: 'internal',
paymentMethodTypes: ['card'],
}),
Expand Down
52 changes: 30 additions & 22 deletions src/pages/guides/multiple-payment-methods.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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, so your route handler stays the same regardless of which method the client chose.

## Server setup

Expand All @@ -69,69 +69,71 @@ 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'],
}),
spark.charge({
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.
The route handler is identical to a single-method setup. `mppx.charge` applies the same options to each registered charge method, advertises every resulting offer in the Challenge, and verifies whichever Credential the client presents. Use [`mppx.compose`](/sdk/typescript/server/Mppx.compose) when offers need different options.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Convert the shared amount before offering Lightning

When the guide registers all three shown methods, applying identical options doesn't produce an equivalent price: src/pages/payment-methods/lightning/charge.mdx:143-144 defines Spark's amount in satoshis with BTC as its default currency, while the Tempo and Stripe defaults here are dollar-denominated. Consequently, the primary route's amount: '0.01' requests 0.01 sat over Lightning rather than the promised $0.01; use explicit composition with a method-specific satoshi amount or perform a currency conversion.

AGENTS.md reference: AGENTS.md:L294-L299

Useful? React with 👍 / 👎.


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

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'],
}),
spark.charge({
mnemonic: process.env.MNEMONIC!,
}),
],
secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'),
})

// [!code focus:start]
Bun.serve({
async fetch(request) {
const result = await mppx.charge({
amount: '0.01',
currency: 'usd',
decimals: 2,
description: 'Premium API access',
})(request)

Expand Down Expand Up @@ -170,34 +172,36 @@ 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'

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'],
}),
spark.charge({
mnemonic: process.env.MNEMONIC!,
}),
],
secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'),
})

app.get(
'/api/resource',
mppx.charge({ amount: '0.01', currency: 'usd', decimals: 2, description: 'Premium API access' }),
mppx.charge({ amount: '0.01', description: 'Premium API access' }),
async (c) => c.json({ message: 'Paid content' }),
)
```
Expand All @@ -208,34 +212,36 @@ 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'

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'],
}),
spark.charge({
mnemonic: process.env.MNEMONIC!,
}),
],
secretKey: process.env.MPP_SECRET_KEY || crypto.randomBytes(32).toString('base64'),
})

app.get(
'/api/resource',
mppx.charge({ amount: '0.01', currency: 'usd', decimals: 2, description: 'Premium API access' }),
mppx.charge({ amount: '0.01', description: 'Premium API access' }),
async (req, res) => res.json({ message: 'Paid content' }),
)
```
Expand All @@ -245,32 +251,34 @@ 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'

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'],
}),
spark.charge({
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' })
mppx.charge({ amount: '0.01', description: 'Premium API access' })
(async () => Response.json({ message: 'Paid content' }))
```

Expand Down
Loading
Loading