Ledger Gate

Examples

Real-world code examples for common @ledgergate/ledgergate-sdk integration patterns — Express, Fastify, x402 payment flows, sampling, and testing.

Basic Express setup

Minimal integration with a single API key from an environment variable:

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

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

const app = express();
app.use(express.json());
app.use(createExpressMiddleware(sdk));

app.get("/api/data", (_req, res) => {
  res.json({ data: "hello" });
});

app.listen(3000, () => console.log("Running on http://localhost:3000"));

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

Basic Fastify setup

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

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

const app = Fastify({ logger: true });
await app.register(fastifyLedgergate, { sdk });

app.get("/api/data", async () => ({ data: "hello" }));

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

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

Tracking an x402 payment endpoint (Express)

This example shows how to emit the full payment lifecycle: payment.required when a client hasn't paid, and payment.verified when they have.

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

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

const app = express();
app.use(express.json());
app.use(createExpressMiddleware(sdk));

const PAYMENT_ADDRESS = "bc1qexampleaddress";
const PRICE_SATS = "1000";

app.get("/api/v1/premium-data", (req, res) => {
  const paymentProof = req.headers["x-payment-proof"];

  if (!paymentProof) {
    // Tell the SDK about the payment metadata by setting response headers
    res
      .status(402)
      .set("x-payment-address", PAYMENT_ADDRESS)
      .set("x-payment-amount", PRICE_SATS)
      .set("x-payment-network", "bitcoin")
      .set("x-payment-token", "SATS")
      .set("x-payment-status", "required")
      .json({ error: "Payment required", amount: PRICE_SATS });
    return;
  }

  // Verified — tell the SDK payment was confirmed
  res
    .set("x-payment-address", PAYMENT_ADDRESS)
    .set("x-payment-amount", PRICE_SATS)
    .set("x-payment-network", "bitcoin")
    .set("x-payment-token", "SATS")
    .set("x-payment-status", "verified")
    .json({ data: "premium content" });
});

app.listen(3000);

Tracking a Lightning (L402) endpoint

The SDK automatically parses WWW-Authenticate: L402 invoice="..." headers — no extra configuration needed:

app.get("/api/lightning-gated", (req, res) => {
  const macaroon = req.headers["authorization"];

  if (!macaroon) {
    // L402 — SDK detects this header pattern automatically
    res
      .status(402)
      .set("WWW-Authenticate", `L402 invoice="lnbc1000n1...", macaroon="abc123"`)
      .json({ error: "Payment required" });
    return;
  }

  res.json({ data: "lightning-gated content" });
});

The SDK will detect the L402 scheme, extract the invoice as the paymentAddress, and set paymentNetwork to "lightning" automatically.


Per-route sampling

Track every payment endpoint at 100%, but only sample 5% of a high-volume health check:

const fullSdk = createLedgergateSdk({ apiKey: process.env.LEDGERGATE_API_KEY!, sampleRate: 1 });
const lowSdk  = createLedgergateSdk({ apiKey: process.env.LEDGERGATE_API_KEY!, sampleRate: 0.05 });

// Full tracking for payment routes
app.use("/api/paid", createExpressMiddleware(fullSdk));

// Sparse sampling for noisy endpoints
app.use("/health", createExpressMiddleware(lowSdk));

// Other routes use the default SDK
app.use(createExpressMiddleware(fullSdk));

Custom header names

If your x402 server uses non-standard header names:

const sdk = createLedgergateSdk({
  apiKey: process.env.LEDGERGATE_API_KEY!,
  x402: {
    source: "header",
    fieldMapping: {
      address: "x-ln-invoice",
      amount:  "x-ln-amount-msat",
      network: "x-ln-chain",
      token:   "x-ln-currency",
      status:  "x-payment-result",
    },
  },
});

Reading payment metadata from the response body

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

// Express route
app.get("/api/paid", (_req, res) => {
  const body = {
    "x-payment-address": "bc1qexample",
    "x-payment-amount": "500",
    "x-payment-network": "bitcoin",
    "x-payment-token": "SATS",
  };

  // Attach the body object so the SDK can inspect it on response finish
  res.locals.x402Body = body;
  res.status(402).json(body);
});

Accessing request context in a handler

The SDK attaches a RequestContext to every request. You can use the id for log correlation:

Express:

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

app.get("/api/data", (req, res) => {
  const ctx = res.locals.x402Context as RequestContext | undefined;
  req.log?.info({ requestId: ctx?.id }, "handling request");
  res.json({ ok: true });
});

Fastify:

app.get("/api/data", async (request) => {
  const { id } = request.x402Context ?? {};
  request.log.info({ requestId: id }, "handling request");
  return { ok: true };
});

IP hashing with a custom salt

Prevent cross-deployment IP correlation by providing a unique salt per environment:

const sdk = createLedgergateSdk({
  apiKey: process.env.LEDGERGATE_API_KEY!,
  redaction: {
    hashIp: true,
    ipHashSalt: process.env.IP_HASH_SALT!, // random secret, unique per deployment
  },
});

Disable IP collection entirely

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

Tuning the transport for high-volume APIs

const sdk = createLedgergateSdk({
  apiKey: process.env.LEDGERGATE_API_KEY!,
  transport: {
    batchSize: 50,          // send batches of 50 events
    flushIntervalMs: 2000,  // flush at least every 2 seconds
    maxRetries: 5,          // more retries for reliability
    timeoutMs: 15_000,      // longer timeout for slower connections
  },
});

Testing — assert events are sent

Force-flush the queue in tests so you can assert on what was sent without waiting for the timer:

import { createLedgergateSdk } from "@ledgergate/ledgergate-sdk";
import { describe, it, expect, vi, afterEach } from "vitest";

describe("payment tracking", () => {
  afterEach(async () => {
    await sdk.shutdown();
  });

  it("emits a payment.required event on 402 response", async () => {
    const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
      new Response(JSON.stringify({ accepted: 1 }), { status: 202 })
    );

    const sdk = createLedgergateSdk({
      apiKey: "test-key",
      sampleRate: 1,
      transport: { batchSize: 100, flushIntervalMs: 60_000 },
    });

    // ... make a request through your app ...

    await sdk.queue.flush();

    expect(fetchSpy).toHaveBeenCalledOnce();
    const [, init] = fetchSpy.mock.calls[0];
    const body = JSON.parse(init?.body as string);
    expect(body[0].eventType).toBe("payment.required");
  });
});

On this page