Skip to content
LogoLogo

Payment hooks

Observe payment lifecycles

Choose a signing account

Create a server-only wallet.ts module, then import account wherever an example creates a local signing account.

wallet.ts
import { privateKeyToAccount } from 'viem/accounts'

export const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)

Payment hooks let you attach logging, metrics, tracing, and request-local context to MPP payment flows without rewriting the payment handler.

Lifecycle overview

Payment hooks follow the 402 payment flow. The server issues a Challenge for an unpaid request. The client selects that Challenge, creates a Credential, and retries the request. The server verifies the Credential, returns the protected response, and attaches a Receipt.

Server hooks

Register server hooks on the object returned by Mppx.create from mppx/server.

HookCanonical eventRuns when
onChallengeCreatedchallenge.createdThe server issues a payment Challenge
onPaymentSuccesspayment.successThe server verifies payment and creates a Receipt
onPaymentFailedpayment.failedA submitted Credential or standalone verification fails
on('*', handler)*Any server payment event fires

Server handlers are awaited inline and sequentially on the payment request path. Handler errors are ignored and do not change payment handling, but slow handlers still delay the response.

server.ts
import { ,  } from 'mppx/server'
 
const  = .({
  : [.(), .()],
})
 
.(({ , ,  }) => { 
  .('challenge.created', {
    : .,
    : .,
    : .,
    : .,
  })
})
 
.(({ , ,  }) => { 
  .('payment.success', {
    : .,
    : .,
    : .,
    : .,
  })
})
 
.(({ , ,  }) => { 
  .('payment.failed', {
    : ?.,
    : .,
    : .,
    : .,
  })
})

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.

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.

HookCanonical eventRuns when
onChallengeReceivedchallenge.receivedA 402 Challenge is selected
onCredentialCreatedcredential.createdA Credential is created for the selected Challenge
onPaymentResponsepayment.responseThe retry after payment returns a successful response
onPaymentFailedpayment.failedChallenge 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 client hooks are observers: thrown errors are ignored and do not change payment handling.

client.ts
import { ,  } from 'mppx/client'
import {  } from 'viem/accounts'
 
const  = ('0x...')
 
const  = .({
  : [
    .({  }),
    .({ , : '10' }),
  ],
  : false,
})
 
.(({  }) => { 
  .('challenge.received', {
    : .,
    : .,
    : .,
  })
})
 
.(({  }) => { 
  .('credential.created', {
    : .,
    : .,
  })
})
 
.(({ ,  }) => { 
  .('payment.response', {
    : .,
    : .,
  })
})
 
.(({ ,  }) => { 
  .('payment.failed', {
    : ?.,
    :  instanceof  ? . : 'Error',
  })
})

Charge intent

For charge, one request maps to one payment. The hook events describe the Challenge, the client Credential, and the server Receipt for that charge.

Use method.intent === 'charge' on server hooks or challenge.intent === 'charge' on client hooks to isolate charge telemetry.

Session intent

For session, hooks observe the MPP request flow around session Challenges and Credentials. Session-specific channel open, voucher, top-up, close, and settlement behavior is handled by the session method APIs; the payment hook names remain the same.

Use method.intent === 'session' on server hooks or challenge.intent === 'session' on client hooks to isolate session telemetry.

server.ts
import { ,  } from 'mppx/server'
 
const  = .({
  : [.()],
})
 
.(({ , ,  }) => { 
  if (. !== 'session') return
 
  .('session.payment.success', {
    : .,
    : .,
    : .,
  })
})

Subscription intent

For subscription, hooks observe the payment flow when the server issues a subscription Challenge and the client returns a subscription Credential. Later requests can be authorized by method-specific subscription state.

Use method.intent === 'subscription' on server hooks or challenge.intent === 'subscription' on client hooks to isolate subscription telemetry.

Event payloads

Payloads carry the selected method, Challenge, request context, and event result. on('*') receives { name, payload }; typed helpers receive the inner payload directly.

server-event.ts
{
  name: 'payment.success',
  payload: {
    challenge: { id: 'ch_123', intent: 'session', method: 'tempo' },
    method: { intent: 'session', name: 'tempo' },
    receipt: {
      method: 'tempo',
      reference: '0x...',
      status: 'success',
      timestamp: '2026-06-24T00:00:00.000Z',
    },
    request: { amount: '0.01' },
  },
}
client-event.ts
{
  name: 'payment.response',
  payload: {
    challenge: { id: 'ch_123', intent: 'session', method: 'tempo' },
    credential: 'Payment ...',
    method: { intent: 'session', name: 'tempo' },
    response: new Response(null, { status: 200 }),
  },
}

Subscription management

Each hook registration returns an unsubscribe function. Keep the function when a handler is temporary, such as request-scoped instrumentation, tests, or a process that recreates payment instances. Call it to detach the handler and stop receiving events.

server.ts
import { ,  } from 'mppx/server'
 
const  = .({
  : [.()],
})
 
const  = .(({  }) => {
  .(.)
})
 
()