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");
});
});