Skip to content

Commit d15c1d7

Browse files
committed
docs: update mppx hooks documentation
1 parent 6a8ccb2 commit d15c1d7

8 files changed

Lines changed: 277 additions & 7 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"@stripe/stripe-js": "^9.3.1",
2626
"@vercel/blob": "^2.3.3",
2727
"mermaid": "^11.14.0",
28-
"mppx": "0.6.20",
28+
"mppx": "0.6.24",
2929
"react": "^19",
3030
"react-dom": "^19",
3131
"stripe": "^22.1.0",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/pages/blog/index.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ imageDescription: "Updates on the Machine Payments Protocol"
1515
import { BlogPostList } from "../../components/BlogPostList";
1616

1717
<BlogPostList posts={[
18+
{
19+
date: "May 18, 2026",
20+
title: "Payment hooks",
21+
description: <>SDKs now expose typed payment hooks for logging, monitoring, and request context.</>,
22+
to: "/blog/payment-hooks",
23+
},
1824
{
1925
date: "May 12, 2026",
2026
title: "Subscriptions",

src/pages/blog/payment-hooks.mdx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
---
2+
layout: minimal
3+
outline: false
4+
showAskAi: false
5+
showFeedback: false
6+
showSearch: false
7+
description: "SDKs now expose typed payment hooks for logging, monitoring, and request context."
8+
imageDescription: "Monitor request status with SDK hooks"
9+
---
10+
11+
<div className="blog-narrow">
12+
13+
<a href="/blog" className="blog-back">Blog</a>
14+
15+
<p className="blog-date" style={{ color: 'var(--vocs-color_text3)', fontSize: '14px' }}>May 18, 2026</p>
16+
17+
# Payment hooks [Observe MPP requests with typed lifecycle events]
18+
19+
SDKs now expose typed payment hooks for client and server payment flows. Use them to record what happened around an MPP request without rewriting your payment handler.
20+
21+
Hooks are useful when payment telemetry belongs next to the rest of your application telemetry: logs, metrics, traces, audit records, support dashboards, or local debugging context.
22+
23+
## What changed
24+
25+
Client hooks observe Challenge selection, Credential creation, retry responses, and failures. Use typed helpers for common events, canonical strings for direct event names, or `*` for a single catch-all handler:
26+
27+
```ts twoslash [client.ts]
28+
import { Mppx, tempo } from 'mppx/client'
29+
import { privateKeyToAccount } from 'viem/accounts'
30+
31+
const account = privateKeyToAccount(process.env.MPP_PRIVATE_KEY as `0x${string}`)
32+
33+
const mppx = Mppx.create({
34+
methods: [tempo({ account })],
35+
polyfill: false,
36+
})
37+
38+
const log = (event: string, data: Record<string, unknown>) => {
39+
console.log(event, data)
40+
}
41+
42+
mppx.onChallengeReceived(({ challenge }) => {
43+
// Observe the selected Challenge before the SDK creates a Credential.
44+
log('payment.challenge.received', {
45+
challengeId: challenge.id,
46+
intent: challenge.intent,
47+
method: challenge.method,
48+
})
49+
return undefined
50+
})
51+
52+
mppx.on('payment.response', ({ challenge, response }) => {
53+
// Use canonical event names when you want to share event wiring.
54+
log('payment.response', {
55+
challengeId: challenge.id,
56+
status: response.status,
57+
})
58+
})
59+
60+
mppx.on('*', ({ name, payload }) => {
61+
// Use `*` to send all payment events through one telemetry path.
62+
log('payment.event', {
63+
hasChallenge: 'challenge' in payload,
64+
name,
65+
})
66+
})
67+
68+
mppx.onPaymentFailed(({ error, input }) => {
69+
// Capture failures from Challenge parsing, Credential creation, or retry handling.
70+
log('payment.failed', {
71+
error: error instanceof Error ? error.message : String(error),
72+
input: String(input),
73+
})
74+
})
75+
76+
const response = await mppx.fetch('https://api.example.com/report')
77+
```
78+
79+
Server hooks observe issued Challenges, successful payments, and rejected Credentials:
80+
81+
```ts twoslash [server.ts]
82+
import { Mppx, tempo } from 'mppx/server'
83+
84+
const payment = Mppx.create({
85+
methods: [tempo.charge()],
86+
})
87+
88+
const log = (event: string, data: Record<string, unknown>) => {
89+
console.log(event, data)
90+
}
91+
92+
payment.onChallengeCreated(({ challenge, request }) => {
93+
// Observe each `402` Challenge before it is returned to the client.
94+
log('payment.challenge.created', {
95+
amount: request.amount,
96+
challengeId: challenge.id,
97+
currency: request.currency,
98+
})
99+
})
100+
101+
payment.on('payment.success', ({ receipt, request }) => {
102+
// Use canonical event names when you want to share event wiring.
103+
log('payment.success', {
104+
amount: request.amount,
105+
currency: request.currency,
106+
reference: receipt.reference,
107+
status: receipt.status,
108+
})
109+
})
110+
111+
payment.on('*', ({ name, payload }) => {
112+
// Use `*` to send all payment events through one telemetry path.
113+
log('payment.event', {
114+
challengeId: payload.challenge.id,
115+
intent: payload.method.intent,
116+
name,
117+
})
118+
})
119+
120+
payment.onPaymentFailed(({ challenge, error, request }) => {
121+
// Record failed Credential verification with request and Challenge context.
122+
log('payment.failed', {
123+
amount: request.amount,
124+
challengeId: challenge.id,
125+
error: error.name,
126+
})
127+
})
128+
```
129+
130+
## Why hooks matter
131+
132+
MPP keeps the payment flow close to the request flow. That makes integration simple, but production systems still need visibility.
133+
134+
Hooks give you a typed place to attach that visibility:
135+
136+
- **Monitoring and observability**: Count Challenges, successful payments, failed Credentials, and paid retry responses, then attach Challenge IDs, method names, intents, amounts, currencies, and Receipt references to traces.
137+
- **Logging**: Record enough context to debug a failed payment without logging secrets.
138+
- **Support**: Connect a user-facing request to the payment attempt that authorized it.
139+
140+
## Use hooks carefully
141+
142+
Client observation hooks don't change payment handling. If a client observation hook throws, `mppx` ignores the error and continues the payment flow.
143+
144+
`onChallengeReceived` is the one client hook that can affect handling: return a non-empty Credential string to use it for the retry. This lets advanced clients create Credentials with custom context before falling back to `onChallenge` or the default flow.
145+
146+
Server hooks run inline on the payment request path. Keep them short, or hand work to your logger, metrics client, or queue. `mppx` ignores thrown server hook errors so observability code doesn't block payment verification, but slow hooks still delay the response.
147+
148+
## What's next
149+
150+
This release starts with core lifecycle events. If there is an event or payload field you need, leave feedback on [GitHub](https://github.com/wevm/mppx/issues).
151+
152+
</div>

src/pages/sdk/typescript/client/Fetch.from.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ Controls when `Accept-Payment` is injected.
6868
6969
Use `'same-origin'` in browsers when you only want same-origin payment discovery. Use `{ origins }` when paid APIs live on specific origins. Origin patterns support `*.` subdomain wildcards.
7070
71+
### eventDispatcher (optional)
72+
73+
- **Type:** `ClientEventDispatcher<methods>`
74+
75+
Advanced shared event dispatcher. `challenge.received` handlers run before `onChallenge`; the first non-empty Credential string skips `onChallenge`.
76+
7177
### fetch (optional)
7278
7379
- **Type:** `typeof globalThis.fetch`

src/pages/sdk/typescript/client/Fetch.polyfill.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ Resolved `Accept-Payment` header and preference data.
6363
6464
Controls when `Accept-Payment` is injected.
6565
66+
### eventDispatcher (optional)
67+
68+
- **Type:** `ClientEventDispatcher<methods>`
69+
70+
Advanced shared event dispatcher. `challenge.received` handlers run before `onChallenge`; the first non-empty Credential string skips `onChallenge`.
71+
6672
### fetch (optional)
6773
6874
- **Type:** `typeof globalThis.fetch`

src/pages/sdk/typescript/client/Mppx.create.mdx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,37 @@ Mppx.create({
1818
const res = await fetch('https://mpp.dev/api/ping/paid')
1919
```
2020

21+
### With payment hooks
22+
23+
Register hooks on the returned `mppx` instance to observe the payment flow.
24+
25+
```ts twoslash
26+
import { Mppx, tempo } from 'mppx/client'
27+
import { privateKeyToAccount } from 'viem/accounts'
28+
29+
const account = privateKeyToAccount('0x...')
30+
31+
const mppx = Mppx.create({
32+
methods: [tempo({ account })],
33+
polyfill: false,
34+
})
35+
36+
const offFailure = mppx.onPaymentFailed(({ error, input }) => {
37+
console.error('payment failed:', input, error)
38+
})
39+
40+
const offResponse = mppx.onPaymentResponse(({ challenge, response }) => {
41+
console.log('payment response:', challenge.id, response.status)
42+
})
43+
44+
const res = await mppx.fetch('https://mpp.dev/api/ping/paid')
45+
console.log(res.status)
46+
// @log: 200
47+
48+
offFailure()
49+
offResponse()
50+
```
51+
2152
### Without polyfill
2253

2354
Set `polyfill: false` to get a scoped fetch without modifying the global:
@@ -103,6 +134,16 @@ type Mppx = {
103134
context?: Context,
104135
options?: { acceptPayment?: string | AcceptPayment.Entry[] },
105136
) => Promise<string>
137+
/** Register a handler for any client payment event. */
138+
on(name: ClientEventName | '*', handler: ClientEventHandler): Unsubscribe
139+
/** Register a handler for received payment Challenges. */
140+
onChallengeReceived(handler: ChallengeReceivedHandler): Unsubscribe
141+
/** Register a handler for created Credentials. */
142+
onCredentialCreated(handler: CredentialCreatedHandler): Unsubscribe
143+
/** Register a handler for failed automatic payment handling. */
144+
onPaymentFailed(handler: PaymentFailedHandler): Unsubscribe
145+
/** Register a handler for payment retry responses. */
146+
onPaymentResponse(handler: PaymentResponseHandler): Unsubscribe
106147
}
107148
```
108149
@@ -125,6 +166,41 @@ const mppx = Mppx.create({
125166
const raw = await mppx.rawFetch('https://api.example.com/ws-auth') // [!code focus]
126167
```
127168

169+
## Payment hooks
170+
171+
Payment hooks are for logging, monitoring, tracing, and request-local context. Each registration returns an unsubscribe function.
172+
173+
| Hook | Runs when |
174+
|---|---|
175+
| `onChallengeReceived` | A `402` Challenge is selected |
176+
| `onCredentialCreated` | A Credential is created for the selected Challenge |
177+
| `onPaymentResponse` | The retry after payment returns a successful response |
178+
| `onPaymentFailed` | Challenge parsing, credential creation, or retry handling fails |
179+
| `on('*', handler)` | Any client payment event fires |
180+
181+
`onChallengeReceived` runs before `onChallenge`. It can return a non-empty Credential string to override the default credential flow. Other hooks are observers: thrown errors are ignored and don't change payment handling.
182+
183+
```ts twoslash
184+
import { Mppx, tempo } from 'mppx/client'
185+
import { privateKeyToAccount } from 'viem/accounts'
186+
187+
const account = privateKeyToAccount('0x...')
188+
189+
const mppx = Mppx.create({
190+
methods: [tempo({ account })],
191+
polyfill: false,
192+
})
193+
194+
mppx.onChallengeReceived(async ({ challenge, createCredential }) => {
195+
console.log('challenge received:', challenge.id)
196+
return createCredential()
197+
})
198+
199+
mppx.on('*', ({ name }) => {
200+
console.log('payment event:', name)
201+
})
202+
```
203+
128204
## Parameters
129205

130206
### acceptPaymentPolicy (optional)

src/pages/sdk/typescript/server/Mppx.create.mdx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,30 @@ const payment = Mppx.create({
2525
})
2626
```
2727

28+
### With payment hooks
29+
30+
Register hooks on the returned `payment` instance to observe Challenges, successful payments, and failures.
31+
32+
```ts twoslash
33+
import { Mppx, tempo } from 'mppx/server'
34+
35+
const payment = Mppx.create({
36+
methods: [tempo.charge()],
37+
})
38+
39+
payment.onChallengeCreated(({ challenge, request }) => {
40+
console.log('challenge created:', challenge.id, request.amount)
41+
})
42+
43+
payment.onPaymentFailed(({ challenge, error }) => {
44+
console.error('payment failed:', challenge.id, error.name)
45+
})
46+
47+
payment.onPaymentSuccess(({ receipt, request }) => {
48+
console.log('payment success:', receipt.reference, request.amount)
49+
})
50+
```
51+
2852
## Return type
2953

3054
```ts
@@ -34,7 +58,7 @@ import type { Mppx, Transport } from 'mppx/server'
3458
type ReturnType = Mppx<[Method.Server], Transport.Http>
3559
```
3660
37-
The returned object includes the method's intent functions (for example, `charge`), `compose`, and `verifyCredential`.
61+
The returned object includes the method's intent functions (for example, `charge`), `challenge`, `compose`, payment hooks, and `verifyCredential`.
3862
3963
## Parameters
4064

0 commit comments

Comments
 (0)