Ledger Gate

Adapters

How to integrate @ledgergate/ledgergate-sdk with Express and Fastify, including advanced usage and accessing request context in route handlers.

Overview

The SDK ships with two framework adapters out of the box:

  • createExpressMiddleware — Express 4 and 5
  • fastifyLedgergate — Fastify 5 (registered as a plugin via fastify-plugin)

Both adapters implement the same lifecycle:

  1. Sampling check — skip all SDK work if not sampled
  2. Build a RequestContext (UUID, redacted headers, hashed IP, timer)
  3. Emit a request.received event
  4. After the response finishes, detect x402 signals and emit the appropriate completion event

All adapter logic is wrapped in try-catch — an error inside the SDK will never propagate to your application.


Express

Basic setup

import express from "express";
import { createLedgergateSdk, createExpressMiddleware } from "@ledgergate/ledgergate-sdk";

const sdk = createLedgergateSdk({
  apiKey: process.env.LEDGERGATE_API_KEY!,
});

const app = express();

// Register before your route handlers
app.use(createExpressMiddleware(sdk));

app.get("/api/v1/price", (_req, res) => {
  res.json({ price: "100 SATS" });
});

app.listen(3000);

Graceful shutdown

Always call sdk.shutdown() before your process exits. This flushes any events still buffered in-memory:

process.on("SIGTERM", async () => {
  await sdk.shutdown();
  process.exit(0);
});

process.on("SIGINT", async () => {
  await sdk.shutdown();
  process.exit(0);
});

Accessing request context in handlers

The middleware stores the RequestContext on res.locals.x402Context. You can read it from any downstream handler or middleware:

import type { RequestContext } from "@ledgergate/ledgergate-sdk";

app.get("/api/data", (req, res) => {
  const context = res.locals.x402Context as RequestContext | undefined;
  console.log("Request ID:", context?.id);
  res.json({ ok: true });
});

Providing a response body for x402 detection

If you configure x402.source: "body" or "both", you need to attach the parsed JSON body to res.locals.x402Body before the response finishes:

app.get("/api/paid", (_req, res) => {
  const paymentBody = {
    "x-payment-address": "bc1qexample...",
    "x-payment-amount": "1000",
    "x-payment-network": "bitcoin",
    "x-payment-token": "BTC",
  };

  // Attach so the SDK can read it on the 'finish' event
  res.locals.x402Body = paymentBody;

  res.status(402).json(paymentBody);
});

Fastify

Basic setup

import Fastify from "fastify";
import { createLedgergateSdk, fastifyLedgergate } from "@ledgergate/ledgergate-sdk";

const sdk = createLedgergateSdk({
  apiKey: process.env.LEDGERGATE_API_KEY!,
});

const app = Fastify();

// Register before your routes
await app.register(fastifyLedgergate, { sdk });

app.get("/api/v1/price", async () => {
  return { price: "100 SATS" };
});

await app.listen({ port: 3000 });

The plugin is wrapped with fastify-plugin so its onRequest and onResponse hooks apply globally, even to routes registered in other Fastify scopes.

Graceful shutdown

process.on("SIGTERM", async () => {
  await app.close();    // closes Fastify and awaits in-flight requests
  await sdk.shutdown(); // flushes remaining events
  process.exit(0);
});

Accessing request context in handlers

The middleware decorates each FastifyRequest with a x402Context property:

import type { RequestContext } from "@ledgergate/ledgergate-sdk";

app.get("/api/data", async (request) => {
  const context: RequestContext | undefined = request.x402Context;
  console.log("Request ID:", context?.id);
  return { ok: true };
});

TypeScript knows about this property automatically because the Fastify adapter extends the FastifyRequest interface via module augmentation.

Providing a response body for x402 detection

Attach the parsed body to request.x402Body inside your handler before returning:

app.get("/api/paid", async (request, reply) => {
  const paymentBody = {
    "x-payment-address": "bc1qexample...",
    "x-payment-amount": "1000",
    "x-payment-network": "bitcoin",
    "x-payment-token": "BTC",
  };

  // Attach so the SDK reads it in the onResponse hook
  request.x402Body = paymentBody;

  return reply.status(402).send(paymentBody);
});

Lifecycle Diagram

Request arrives


┌─────────────────────────────────┐
│  Adapter middleware / onRequest  │
│  1. shouldSample()               │
│  2. createRequestContext()       │
│  3. enqueue(request.received)    │
└────────────────┬────────────────┘


         Your route handler


┌─────────────────────────────────┐
│  Response finish / onResponse    │
│  4. captureResponseData()        │
│  5. detectX402()                 │
│  6. enqueue(payment.* or        │
│             request.completed)  │
└────────────────┬────────────────┘


          EventQueue (async)

TypeScript Reference

import {
  createExpressMiddleware,
  fastifyLedgergate,
  type FastifyLedgergateOptions,
  type SdkInstance,
} from "@ledgergate/ledgergate-sdk";
ExportDescription
createExpressMiddleware(sdk)Returns an Express RequestHandler
fastifyLedgergateFastify plugin (FastifyPluginAsync)
FastifyLedgergateOptions{ sdk: SdkInstance }
SdkInstanceInterface for the object returned by createLedgergateSdk()

On this page