diff --git a/package.json b/package.json index 2a81cbcc..51d90e20 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,12 @@ "@stripe/stripe-js": "^9.3.1", "@vercel/blob": "^2.3.3", "mermaid": "^11.14.0", - "mppx": "0.6.20", + "mppx": "0.6.27", "react": "^19", "react-dom": "^19", "stripe": "^22.1.0", "tailwindcss": "^4.2.4", - "viem": "^2.48.4", + "viem": "^2.50.4", "vocs": "https://pkg.pr.new/vocs@425", "wagmi": "^3.6.5", "waku": "^1.0.0-alpha.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bba8f342..c8ef8403 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,8 +29,8 @@ importers: specifier: ^11.14.0 version: 11.14.0 mppx: - specifier: 0.6.20 - version: 0.6.20(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6))(express@5.2.1)(hono@4.12.18)(typescript@6.0.3)(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)) + specifier: 0.6.27 + version: 0.6.27(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6))(express@5.2.1)(hono@4.12.18)(typescript@6.0.3)(viem@2.50.4(typescript@6.0.3)(zod@4.3.6)) react: specifier: ^19 version: 19.2.5 @@ -44,14 +44,14 @@ importers: specifier: ^4.2.4 version: 4.2.4 viem: - specifier: ^2.48.4 - version: 2.48.4(typescript@6.0.3)(zod@4.3.6) + specifier: ^2.50.4 + version: 2.50.4(typescript@6.0.3)(zod@4.3.6) vocs: specifier: https://pkg.pr.new/vocs@425 version: https://pkg.pr.new/vocs@425(patch_hash=5b340733de5440b4e6ed05bb7181d1dcb81e595422ba76d685a4c14062177323)(@cfworker/json-schema@4.1.1)(@date-fns/tz@1.4.1)(@types/react@19.2.14)(date-fns@4.1.0)(mermaid@11.14.0)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.2(esbuild@0.27.7)))(react@19.2.5)(rollup@4.60.1)(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))(waku@1.0.0-alpha.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.2(esbuild@0.27.7)))(react@19.2.5)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0)) wagmi: specifier: ^3.6.5 - version: 3.6.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)) + version: 3.6.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(viem@2.50.4(typescript@6.0.3)(zod@4.3.6)) waku: specifier: ^1.0.0-alpha.8 version: 1.0.0-alpha.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.2(esbuild@0.27.7)))(react@19.2.5)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0) @@ -3022,15 +3022,15 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mppx@0.6.20: - resolution: {integrity: sha512-RNXcY64QQVuDo3BqFEQAfp8ciF9O+DVIazDzZsf8vd9g4xqO1BRvTBLEcSO0WcEo6HcmQSL3Qsv3zgdM8vb3Eg==} + mppx@0.6.27: + resolution: {integrity: sha512-7KxM+Uau7dDcOBI9RjJYSLcDlF7glAC09eX6h64AopmQ9zZXN5gicSg7Ty8hmutXkUglEFPG/8YfWXGK4CQXSw==} hasBin: true peerDependencies: '@modelcontextprotocol/sdk': '>=1.25.0' elysia: '>=1' express: '>=5' hono: '>=4.12.18' - viem: '>=2.47.5' + viem: '>=2.50.4' peerDependenciesMeta: '@modelcontextprotocol/sdk': optional: true @@ -3116,8 +3116,8 @@ packages: outvariant@1.4.0: resolution: {integrity: sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==} - ox@0.14.20: - resolution: {integrity: sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw==} + ox@0.14.22: + resolution: {integrity: sha512-nb5msL8qWbPglhIfZbGJAfw3cqiJjFMiWmACt7kgyWtLib12tcctbHufMT9Hb0Lr6Pt4k9I3dbpueTpbhvbqvA==} peerDependencies: typescript: '>=5.4.0' peerDependenciesMeta: @@ -3778,8 +3778,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - viem@2.48.4: - resolution: {integrity: sha512-mReP/rgY2P+WeeRSG4sUvccCLKfyAW1C73Y3KkobAqgzYmVna9qyUMNE44xIUkDtfvRuC33r24UhF4baBYovsg==} + viem@2.50.4: + resolution: {integrity: sha512-rf98F4s3Vlb+uJZEKfay3IbBw3CNCbVtx5Y3UIljlO2tSX420g/J0WQSYsjzBSasUFgxgsXabji14O9kGbiqgg==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -3994,8 +3994,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4006,8 +4006,8 @@ packages: utf-8-validate: optional: true - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -5479,18 +5479,18 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@wagmi/connectors@8.0.5(@wagmi/core@3.4.6(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)))(typescript@6.0.3)(viem@2.48.4(typescript@6.0.3)(zod@4.3.6))': + '@wagmi/connectors@8.0.5(@wagmi/core@3.4.6(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.50.4(typescript@6.0.3)(zod@4.3.6)))(typescript@6.0.3)(viem@2.50.4(typescript@6.0.3)(zod@4.3.6))': dependencies: - '@wagmi/core': 3.4.6(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)) - viem: 2.48.4(typescript@6.0.3)(zod@4.3.6) + '@wagmi/core': 3.4.6(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.50.4(typescript@6.0.3)(zod@4.3.6)) + viem: 2.50.4(typescript@6.0.3)(zod@4.3.6) optionalDependencies: typescript: 6.0.3 - '@wagmi/core@3.4.6(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(typescript@6.0.3)(zod@4.3.6))': + '@wagmi/core@3.4.6(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.50.4(typescript@6.0.3)(zod@4.3.6))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@6.0.3) - viem: 2.48.4(typescript@6.0.3)(zod@4.3.6) + viem: 2.50.4(typescript@6.0.3)(zod@4.3.6) zustand: 5.0.0(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.4.0(react@19.2.5)) optionalDependencies: '@tanstack/query-core': 5.90.20 @@ -6510,9 +6510,9 @@ snapshots: isexe@2.0.0: {} - isows@1.0.7(ws@8.18.3): + isows@1.0.7(ws@8.20.1): dependencies: - ws: 8.18.3 + ws: 8.20.1 jest-worker@27.5.1: dependencies: @@ -7226,11 +7226,11 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - mppx@0.6.20(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6))(express@5.2.1)(hono@4.12.18)(typescript@6.0.3)(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)): + mppx@0.6.27(@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6))(express@5.2.1)(hono@4.12.18)(typescript@6.0.3)(viem@2.50.4(typescript@6.0.3)(zod@4.3.6)): dependencies: incur: 0.4.5 - ox: 0.14.20(typescript@6.0.3)(zod@4.4.3) - viem: 2.48.4(typescript@6.0.3)(zod@4.3.6) + ox: 0.14.22(typescript@6.0.3)(zod@4.4.3) + viem: 2.50.4(typescript@6.0.3)(zod@4.3.6) zod: 4.4.3 optionalDependencies: '@modelcontextprotocol/sdk': 1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6) @@ -7291,7 +7291,7 @@ snapshots: outvariant@1.4.0: {} - ox@0.14.20(typescript@6.0.3)(zod@4.3.6): + ox@0.14.22(typescript@6.0.3)(zod@4.3.6): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -7306,7 +7306,7 @@ snapshots: transitivePeerDependencies: - zod - ox@0.14.20(typescript@6.0.3)(zod@4.4.3): + ox@0.14.22(typescript@6.0.3)(zod@4.4.3): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -8067,16 +8067,16 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - viem@2.48.4(typescript@6.0.3)(zod@4.3.6): + viem@2.50.4(typescript@6.0.3)(zod@4.3.6): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 abitype: 1.2.3(typescript@6.0.3)(zod@4.3.6) - isows: 1.0.7(ws@8.18.3) - ox: 0.14.20(typescript@6.0.3)(zod@4.3.6) - ws: 8.18.3 + isows: 1.0.7(ws@8.20.1) + ox: 0.14.22(typescript@6.0.3)(zod@4.3.6) + ws: 8.20.1 optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -8262,14 +8262,14 @@ snapshots: w3c-keyname@2.2.8: {} - wagmi@3.6.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)): + wagmi@3.6.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(viem@2.50.4(typescript@6.0.3)(zod@4.3.6)): dependencies: '@tanstack/react-query': 5.90.21(react@19.2.5) - '@wagmi/connectors': 8.0.5(@wagmi/core@3.4.6(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)))(typescript@6.0.3)(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)) - '@wagmi/core': 3.4.6(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)) + '@wagmi/connectors': 8.0.5(@wagmi/core@3.4.6(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.50.4(typescript@6.0.3)(zod@4.3.6)))(typescript@6.0.3)(viem@2.50.4(typescript@6.0.3)(zod@4.3.6)) + '@wagmi/core': 3.4.6(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.5)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.50.4(typescript@6.0.3)(zod@4.3.6)) react: 19.2.5 use-sync-external-store: 1.4.0(react@19.2.5) - viem: 2.48.4(typescript@6.0.3)(zod@4.3.6) + viem: 2.50.4(typescript@6.0.3)(zod@4.3.6) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -8378,10 +8378,10 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.3: {} - ws@8.20.0: {} + ws@8.20.1: {} + yallist@3.1.1: {} yaml@2.9.0: {} diff --git a/src/pages/blog/index.mdx b/src/pages/blog/index.mdx index 0593a657..b7fe1110 100644 --- a/src/pages/blog/index.mdx +++ b/src/pages/blog/index.mdx @@ -15,6 +15,12 @@ imageDescription: "Updates on the Machine Payments Protocol" import { BlogPostList } from "../../components/BlogPostList"; Core SDKs now expose typed payment hooks for logging, monitoring, and request context., + to: "/blog/payment-hooks", + }, { date: "May 12, 2026", title: "Subscriptions", diff --git a/src/pages/blog/payment-hooks.mdx b/src/pages/blog/payment-hooks.mdx new file mode 100644 index 00000000..1b830e38 --- /dev/null +++ b/src/pages/blog/payment-hooks.mdx @@ -0,0 +1,73 @@ +--- +layout: minimal +outline: false +showAskAi: false +showFeedback: false +showSearch: false +description: "Core SDKs now expose typed payment hooks for logging, monitoring, and request context." +imageDescription: "Monitor request status with SDK hooks" +--- + +
+ +Blog + +

Thursday, May 21, 2026

+ +# Payment hooks [Observe MPP requests with typed lifecycle events] + +MPP's core 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. + +## Why hooks matter + +MPP puts the payment flow close to the request flow in REST APIs and MCP servers. That makes basic integration simple, but becomes fragile when used in production applications and existing tech stacks. + +Hooks give you a typed place to attach that visibility: + +- **Monitoring and observability**: Count `Challenge`s, successful payments, failed `Credential`s, and paid retry responses, then attach `Challenge` IDs, method names, intents, amounts, currencies, and `Receipt` references to traces. +- **Logging**: Record enough context to debug a failed payment without logging secrets. +- **Support**: Connect a user-facing request to the payment attempt that authorized it. + +## What changed + +Server hooks observe issued Challenges, successful payments, and rejected Credentials. Use typed helpers for common events, canonical strings for direct event names, or `*` for a single catch-all handler: + +```ts twoslash [server.ts] +import { Mppx, tempo } from 'mppx/server' + +const payment = Mppx.create({ + methods: [tempo.charge()], +}) + +const log = (event: string, data: Record) => { + console.log(event, data) +} + +payment.onChallengeCreated(({ challenge, request }) => { // [!code hl] + log('payment.challenge.created', { + amount: request.amount, + challengeId: challenge.id, + }) +}) + +payment.on('payment.success', ({ receipt, request }) => { // [!code hl] + log('payment.success', { + amount: request.amount, + reference: receipt.reference, + }) +}) + +payment.onPaymentFailed(({ challenge, error, request }) => { // [!code hl] + log('payment.failed', { + amount: request.amount, + challengeId: challenge.id, + error: error.name, + }) +}) +``` + +## What's next + +This release starts with core lifecycle events of the MPP request flow. If there is an event or payload field which you would like to monitor, please leave feedback on [GitHub](https://github.com/wevm/mppx/issues). + +
diff --git a/src/pages/sdk/features.mdx b/src/pages/sdk/features.mdx index ad5f80eb..73d01bb8 100644 --- a/src/pages/sdk/features.mdx +++ b/src/pages/sdk/features.mdx @@ -13,15 +13,16 @@ This page tracks which features are implemented in each official SDK. | Component | [TypeScript](https://github.com/wevm/mppx) | [Rust](https://github.com/tempoxyz/mpp-rs) | [Python](https://github.com/tempoxyz/pympp) | [Ruby](https://github.com/stripe/mpp-rb) | |---|---|---|---|---| | Client | ✓ | ✓ | ✓ | ✓ | -| Server | ✓ | ✓ | ✓ | ✓ | +| Event handling | ✓ | ✓ | ✓ | ✓ | | Proxy | ✓ | ✓ | — | — | +| Server | ✓ | ✓ | ✓ | ✓ | ## Payment methods | Method | TypeScript | Rust | Python | Ruby | |---|---|---|---|---| | [Tempo](/payment-methods/tempo) | ✓ | ✓ | ✓ | ✓ | -| [Stripe](/payment-methods/stripe) | ✓ | — | — | ✓ | +| [Stripe](/payment-methods/stripe) | ✓ | ✓ | ✓ | ✓ | Additional payment methods implement their own SDKs. Refer to the maintaining organizations for support and availability. diff --git a/src/pages/sdk/index.mdx b/src/pages/sdk/index.mdx index 3bc8d346..08c9712f 100644 --- a/src/pages/sdk/index.mdx +++ b/src/pages/sdk/index.mdx @@ -25,6 +25,7 @@ import { TypeScriptSdkCard, PythonSdkCard, RustSdkCard, GoSdkCard, RubySdkCard, | **Server** | | | | | | | **Core types** | | | | | | | **Charge intent** | | | | | | +| **Event handling** | | | | | | | **Session intent** | | | | | | | **Stripe method** | | | | | | | **Fee sponsorship** | | | | | | diff --git a/src/pages/sdk/python/client.mdx b/src/pages/sdk/python/client.mdx index e0613e1e..2c6a965b 100644 --- a/src/pages/sdk/python/client.mdx +++ b/src/pages/sdk/python/client.mdx @@ -21,6 +21,50 @@ async with Client(methods=[tempo(account=account, intents={"charge": ChargeInten print(response.json()) ``` +## Event handling + +Register event handlers to record the automatic `402` flow. Use helper methods for named events, event constants for shared wiring, and `*` for every client event. + +```python [client.py] +from mpp.client import Client +from mpp.events import PAYMENT_RESPONSE +from mpp.methods.tempo import tempo, TempoAccount, ChargeIntent + +account = TempoAccount.from_key("0x...") + +async with Client(methods=[tempo(account=account, intents={"charge": ChargeIntent()})]) as client: + def log(event: str, data: dict[str, object]) -> None: + print(event, data) + + # Record the selected Challenge before the client creates a Credential. + client.on_challenge_received( + lambda payload: log( + "payment.challenge.received", + { + "challenge_id": payload["challenge"].id, + "intent": payload["challenge"].intent, + "method": payload["challenge"].method, + }, + ) + ) + + # Record the retried response after payment. + client.on( + PAYMENT_RESPONSE, + lambda payload: log( + "payment.response", + {"status": payload["response"].status_code}, + ), + ) + + # Catch every client payment event in one handler. + client.on("*", lambda event: log("payment.event", {"name": event.name})) + + response = await client.get("https://api.example.com/resource") +``` + +`on_challenge_received` can return a `Credential` to override the default Credential creation path. Other handlers only record payment handling. Each registration returns an unsubscribe function. + ## Common methods | Method | Description | diff --git a/src/pages/sdk/python/server.mdx b/src/pages/sdk/python/server.mdx index b3c22c4e..512d081b 100644 --- a/src/pages/sdk/python/server.mdx +++ b/src/pages/sdk/python/server.mdx @@ -62,6 +62,57 @@ mpp = Mpp.create( ) ``` +## Event handling + +Register event handlers to record issued Challenges, successful payments, and rejected Credentials. Use helper methods for named events, event constants for shared wiring, and `*` for every server event. + +```python [server.py] +from mpp.events import PAYMENT_SUCCESS +from mpp.methods.tempo import tempo, ChargeIntent +from mpp.server import Mpp + +mpp = Mpp.create( + method=tempo( + currency="0x20c0000000000000000000000000000000000000", + intents={"charge": ChargeIntent()}, + recipient="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + ), + secret_key="my-server-secret", +) + +def log(event: str, data: dict[str, object]) -> None: + print(event, data) + +# Record each Challenge issued by your API. +mpp.on_challenge_created( + lambda payload: log( + "payment.challenge.created", + { + "amount": payload["request"]["amount"], + "currency": payload["request"]["currency"], + "method": payload["method"], + }, + ) +) + +# Record successful Credential verification. +mpp.on( + PAYMENT_SUCCESS, + lambda payload: log( + "payment.success", + { + "intent": payload["intent"], + "reference": payload["receipt"].reference, + }, + ), +) + +# Catch every server payment event in one handler. +mpp.on("*", lambda event: log("payment.event", {"name": event.name})) +``` + +Server handlers run inline on the payment request path. Keep them short, or send work to your logger, metrics client, or queue. Each registration returns an unsubscribe function. + ## `tempo()` factory `tempo()` creates a `TempoMethod` that bundles a payment network, its intents, and default parameters together. Each intent must be explicitly registered via the `intents` parameter. diff --git a/src/pages/sdk/ruby/client.mdx b/src/pages/sdk/ruby/client.mdx index abc3b695..a1dd0013 100644 --- a/src/pages/sdk/ruby/client.mdx +++ b/src/pages/sdk/ruby/client.mdx @@ -28,6 +28,50 @@ When the server returns `402`, the client: 2. Calls `create_credential` on the matching method to sign a stablecoin transfer 3. Retries the request with the Credential in the `Authorization` header +## Event handling + +Register event handlers to record the automatic `402` flow. Use helper methods for named events, event constants for shared wiring, and `*` for every client event. + +```ruby [client.rb] +require "json" +require "mpp-rb" + +account = Mpp::Methods::Tempo::Account.from_key("0x...") + +transport = Mpp::Client::Transport.new( + methods: [Mpp::Methods::Tempo.tempo(account: account)], +) + +log = ->(event, data) { puts({ event: event, **data }.to_json) } + +# Record the selected Challenge before the client creates a Credential. +transport.on_challenge_received do |payload| + log.call( + "payment.challenge.received", + { + challenge_id: payload[:challenge].id, + intent: payload[:challenge].intent, + method: payload[:challenge].method, + }, + ) + nil +end + +# Record the retried response after payment. +transport.on(Mpp::Events::PAYMENT_RESPONSE) do |payload| + log.call("payment.response", { status: payload[:response].code }) +end + +# Catch every client payment event in one handler. +transport.on("*") do |event| + log.call("payment.event", { name: event.name }) +end + +response = transport.get("https://api.example.com/resource") +``` + +`on_challenge_received` can return a `Credential` or `Payment` authorization string to override the default Credential creation path. Other handlers only record payment handling. Each registration returns an unsubscribe proc. + ## Common methods | Method | Description | diff --git a/src/pages/sdk/ruby/server.mdx b/src/pages/sdk/ruby/server.mdx index 8cd6181b..406d3942 100644 --- a/src/pages/sdk/ruby/server.mdx +++ b/src/pages/sdk/ruby/server.mdx @@ -50,6 +50,56 @@ server = Mpp.create( ) ``` +## Event handling + +Register event handlers to record issued Challenges, successful payments, and rejected Credentials. Use helper methods for named events, event constants for shared wiring, and `*` for every server event. + +```ruby [server.rb] +require "json" +require "mpp-rb" + +server = Mpp.create( + method: Mpp::Methods::Tempo.tempo( + currency: "0x20c0000000000000000000000000000000000000", + intents: { "charge" => Mpp::Methods::Tempo::ChargeIntent.new }, + recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + ), + secret_key: "my-server-secret", +) + +log = ->(event, data) { puts({ event: event, **data }.to_json) } + +# Record each Challenge issued by your API. +server.on_challenge_created do |payload| + log.call( + "payment.challenge.created", + { + amount: payload[:request]["amount"], + currency: payload[:request]["currency"], + method: payload[:method][:name], + }, + ) +end + +# Record successful Credential verification. +server.on(Mpp::Events::PAYMENT_SUCCESS) do |payload| + log.call( + "payment.success", + { + intent: payload[:method][:intent], + reference: payload[:receipt].reference, + }, + ) +end + +# Catch every server payment event in one handler. +server.on("*") do |event| + log.call("payment.event", { name: event.name }) +end +``` + +Server handlers run inline on the payment request path. Keep them short, or send work to your logger, metrics client, or queue. Each registration returns an unsubscribe proc. + ## `Mpp::Methods::Tempo.tempo()` factory `tempo()` creates a method that bundles a payment network, its intents, and default parameters together. Each intent must be explicitly registered via the `intents` parameter. diff --git a/src/pages/sdk/rust/client.mdx b/src/pages/sdk/rust/client.mdx index 245cd8d3..ffc2b8d0 100644 --- a/src/pages/sdk/rust/client.mdx +++ b/src/pages/sdk/rust/client.mdx @@ -58,7 +58,7 @@ let provider = TempoProvider::new(signer, "https://rpc.tempo.xyz")? For automatic `402` handling on all requests, use `PaymentMiddleware` with `reqwest-middleware`. Requires the `middleware` feature. ```toml -mpp = { version = "0.1", features = ["tempo", "client", "middleware"] } +mpp = { version = "0.10.3", features = ["tempo", "client", "middleware"] } ``` ```rust @@ -75,6 +75,44 @@ let client = ClientBuilder::new(reqwest::Client::new()) let response = client.get("https://api.example.com/paid").send().await?; ``` +## Event handling + +Use `ClientEvents` to record the automatic `402` flow. Use helper methods for named events, `on()` for one event kind, and `on_any()` for every client event. + +```rust +use mpp::client::{ + ClientEvent, ClientEventKind, ClientEvents, PaymentMiddleware, TempoProvider, +}; +use reqwest_middleware::ClientBuilder; + +let provider = TempoProvider::new(signer, "https://rpc.tempo.xyz")?; +let events = ClientEvents::default(); + +// Record the selected Challenge before the provider creates a Credential. +let _challenge = events.on_challenge_received(|ctx| async move { + println!("challenge received: {}", ctx.challenge.id); + None +}); + +// Record the retried response after payment. +let _response = events.on(ClientEventKind::PaymentResponse, |event| async move { + if let ClientEvent::PaymentResponse(ctx) = event { + println!("payment response: {}", ctx.status); + } +}); + +// Catch every client payment event in one handler. +let _all = events.on_any(|event| async move { + println!("payment event: {}", event.kind().as_str()); +}); + +let client = ClientBuilder::new(reqwest::Client::new()) + .with(PaymentMiddleware::new(provider).with_events(events)) + .build(); +``` + +`on_challenge_received` can return `Some(PaymentCredential)` to override the default provider payment flow. Other handlers only record payment handling. Keep the returned subscription handles alive while handlers remain registered. + ## Multiple providers `MultiProvider` wraps multiple payment providers and picks the right one based on the challenge's `method` and `intent`: diff --git a/src/pages/sdk/rust/index.mdx b/src/pages/sdk/rust/index.mdx index 3fa8d668..0edf220b 100644 --- a/src/pages/sdk/rust/index.mdx +++ b/src/pages/sdk/rust/index.mdx @@ -89,16 +89,16 @@ let response = reqwest::Client::new() ```toml # Client only -mpp = { version = "0.1", features = ["tempo", "client"] } +mpp = { version = "0.10.3", features = ["tempo", "client"] } # Server only -mpp = { version = "0.1", features = ["tempo", "server"] } +mpp = { version = "0.10.3", features = ["tempo", "server"] } # Both -mpp = { version = "0.1", features = ["tempo", "client", "server"] } +mpp = { version = "0.10.3", features = ["tempo", "client", "server"] } # With middleware -mpp = { version = "0.1", features = ["tempo", "client", "middleware"] } +mpp = { version = "0.10.3", features = ["tempo", "client", "middleware"] } ``` ## Next steps diff --git a/src/pages/sdk/rust/server.mdx b/src/pages/sdk/rust/server.mdx index ec0aba49..b2d1ba2c 100644 --- a/src/pages/sdk/rust/server.mdx +++ b/src/pages/sdk/rust/server.mdx @@ -204,6 +204,36 @@ let receipt = mpp .await?; ``` +## Event handling + +Register event handlers to record successful Credential verification. Use the helper method for `payment.success`, `on()` for one event kind, and `on_any()` for every server event. + +```rust +use mpp::server::{Mpp, ServerEvent, ServerEventKind, TempoConfig, tempo}; + +let mpp = Mpp::create(tempo(TempoConfig { + recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", +}))?; + +// Record successful Credential verification. +let _success = mpp.on_payment_success(|ctx| async move { + println!("payment success: {}", ctx.receipt.reference); +}); + +// Register by event kind when you want direct event access. +let _by_kind = mpp.on(ServerEventKind::PaymentSuccess, |event| async move { + let ServerEvent::PaymentSuccess(ctx) = event; + println!("payment method: {}", ctx.method); +}); + +// Catch every server payment event in one handler. +let _all = mpp.on_any(|event| async move { + println!("payment event: {}", event.kind().as_str()); +}); +``` + +Server handlers run inline with verification. Keep the returned subscription handles alive while handlers remain registered. + ## Axum extractor The `MppCharge` extractor handles the full `402` challenge/verify flow automatically. Requires the `axum` feature. diff --git a/src/pages/sdk/typescript/client/Fetch.from.mdx b/src/pages/sdk/typescript/client/Fetch.from.mdx index 73718926..3188f5a0 100644 --- a/src/pages/sdk/typescript/client/Fetch.from.mdx +++ b/src/pages/sdk/typescript/client/Fetch.from.mdx @@ -68,6 +68,12 @@ Controls when `Accept-Payment` is injected. 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. +### eventDispatcher (optional) + +- **Type:** `ClientEventDispatcher` + +Advanced shared event dispatcher. `challenge.received` handlers run before `onChallenge`; the first non-empty Credential string skips `onChallenge`. + ### fetch (optional) - **Type:** `typeof globalThis.fetch` diff --git a/src/pages/sdk/typescript/client/Fetch.polyfill.mdx b/src/pages/sdk/typescript/client/Fetch.polyfill.mdx index 4a0d47a6..0b0930ca 100644 --- a/src/pages/sdk/typescript/client/Fetch.polyfill.mdx +++ b/src/pages/sdk/typescript/client/Fetch.polyfill.mdx @@ -63,6 +63,12 @@ Resolved `Accept-Payment` header and preference data. Controls when `Accept-Payment` is injected. +### eventDispatcher (optional) + +- **Type:** `ClientEventDispatcher` + +Advanced shared event dispatcher. `challenge.received` handlers run before `onChallenge`; the first non-empty Credential string skips `onChallenge`. + ### fetch (optional) - **Type:** `typeof globalThis.fetch` diff --git a/src/pages/sdk/typescript/client/Mppx.create.mdx b/src/pages/sdk/typescript/client/Mppx.create.mdx index d22492e6..39df45c2 100644 --- a/src/pages/sdk/typescript/client/Mppx.create.mdx +++ b/src/pages/sdk/typescript/client/Mppx.create.mdx @@ -18,6 +18,37 @@ Mppx.create({ const res = await fetch('https://mpp.dev/api/ping/paid') ``` +### With payment hooks + +Register hooks on the returned `mppx` instance to observe the payment flow. + +```ts twoslash +import { Mppx, tempo } from 'mppx/client' +import { privateKeyToAccount } from 'viem/accounts' + +const account = privateKeyToAccount('0x...') + +const mppx = Mppx.create({ + methods: [tempo({ account })], + polyfill: false, +}) + +const offFailure = mppx.onPaymentFailed(({ error, input }) => { + console.error('payment failed:', input, error) +}) + +const offResponse = mppx.onPaymentResponse(({ challenge, response }) => { + console.log('payment response:', challenge.id, response.status) +}) + +const res = await mppx.fetch('https://mpp.dev/api/ping/paid') +console.log(res.status) +// @log: 200 + +offFailure() +offResponse() +``` + ### Without polyfill Set `polyfill: false` to get a scoped fetch without modifying the global: @@ -103,6 +134,16 @@ type Mppx = { context?: Context, options?: { acceptPayment?: string | AcceptPayment.Entry[] }, ) => Promise + /** Register a handler for any client payment event. */ + on(name: ClientEventName | '*', handler: ClientEventHandler): Unsubscribe + /** Register a handler for received payment Challenges. */ + onChallengeReceived(handler: ChallengeReceivedHandler): Unsubscribe + /** Register a handler for created Credentials. */ + onCredentialCreated(handler: CredentialCreatedHandler): Unsubscribe + /** Register a handler for failed automatic payment handling. */ + onPaymentFailed(handler: PaymentFailedHandler): Unsubscribe + /** Register a handler for payment retry responses. */ + onPaymentResponse(handler: PaymentResponseHandler): Unsubscribe } ``` @@ -125,6 +166,41 @@ const mppx = Mppx.create({ const raw = await mppx.rawFetch('https://api.example.com/ws-auth') // [!code focus] ``` +## Payment hooks + +Payment hooks are for logging, monitoring, tracing, and request-local context. Each registration returns an unsubscribe function. + +| Hook | Runs when | +|---|---| +| `onChallengeReceived` | A `402` Challenge is selected | +| `onCredentialCreated` | A Credential is created for the selected Challenge | +| `onPaymentResponse` | The retry after payment returns a successful response | +| `onPaymentFailed` | Challenge parsing, credential creation, or retry handling fails | +| `on('*', handler)` | Any client payment event fires | + +`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. + +```ts twoslash +import { Mppx, tempo } from 'mppx/client' +import { privateKeyToAccount } from 'viem/accounts' + +const account = privateKeyToAccount('0x...') + +const mppx = Mppx.create({ + methods: [tempo({ account })], + polyfill: false, +}) + +mppx.onChallengeReceived(async ({ challenge, createCredential }) => { + console.log('challenge received:', challenge.id) + return createCredential() +}) + +mppx.on('*', ({ name }) => { + console.log('payment event:', name) +}) +``` + ## Parameters ### acceptPaymentPolicy (optional) diff --git a/src/pages/sdk/typescript/server/Mppx.create.mdx b/src/pages/sdk/typescript/server/Mppx.create.mdx index 9e72e8f5..bbe460c9 100644 --- a/src/pages/sdk/typescript/server/Mppx.create.mdx +++ b/src/pages/sdk/typescript/server/Mppx.create.mdx @@ -25,6 +25,30 @@ const payment = Mppx.create({ }) ``` +### With payment hooks + +Register hooks on the returned `payment` instance to observe Challenges, successful payments, and failures. + +```ts twoslash +import { Mppx, tempo } from 'mppx/server' + +const payment = Mppx.create({ + methods: [tempo.charge()], +}) + +payment.onChallengeCreated(({ challenge, request }) => { + console.log('challenge created:', challenge.id, request.amount) +}) + +payment.onPaymentFailed(({ challenge, error }) => { + console.error('payment failed:', challenge.id, error.name) +}) + +payment.onPaymentSuccess(({ receipt, request }) => { + console.log('payment success:', receipt.reference, request.amount) +}) +``` + ## Return type ```ts @@ -34,7 +58,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`), `compose`, and `verifyCredential`. +The returned object includes the method's intent functions (for example, `charge`), `challenge`, `compose`, payment hooks, and `verifyCredential`. ## Parameters