Ledger Gate

Advanced Usage

Sampling strategies, manual flushing, graceful shutdown, L402/LSAT support, and low-level API access in @ledgergate/ledgergate-sdk.

Graceful Shutdown

The SDK holds an in-memory event buffer. When your process exits, any events still in the buffer will be lost unless you explicitly flush them.

Always call sdk.shutdown() in your process exit handlers:

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

async function shutdown() {
  await sdk.shutdown(); // flushes buffer and waits for in-flight HTTP requests
  process.exit(0);
}

process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);

sdk.shutdown() is idempotent — calling it multiple times is safe. After shutdown, the queue rejects new events silently (if debug: true a warning is logged).

With Fastify

Fastify's app.close() waits for in-flight requests to complete, so pair it with sdk.shutdown():

process.on("SIGTERM", async () => {
  await app.close();    // drain connections first
  await sdk.shutdown(); // then flush remaining events
  process.exit(0);
});

Manual Flush

You can trigger an immediate flush of the event buffer at any time without shutting down:

// Force-send all buffered events right now
await sdk.queue.flush();

This is useful in test environments where you need to assert that events were sent before a test assertion runs.


Queue Inspection

The event queue exposes a size() method to inspect the current buffer length:

const buffered = sdk.queue.size();
console.log(`${buffered} events pending`);

Manually enqueuing events

The queue interface also accepts arbitrary AnalyticsEvent objects directly. This is an escape hatch for building custom adapters:

import { buildRequestReceivedEvent } from "@ledgergate/ledgergate-sdk";

const event = buildRequestReceivedEvent(context);
sdk.queue.enqueue(event);

Sampling Strategies

Global rate

Set a single rate for all traffic:

const sdk = createLedgergateSdk({
  apiKey: process.env.LEDGERGATE_API_KEY!,
  sampleRate: 0.1, // 10%
});

Per-route sampling (Express)

Create multiple SDK instances with different rates and mount them on specific route prefixes:

const fullSdk = createLedgergateSdk({ apiKey: "...", sampleRate: 1 });
const lowSdk  = createLedgergateSdk({ apiKey: "...", sampleRate: 0.05 });

// Track every payment endpoint
app.use("/api/paid", createExpressMiddleware(fullSdk));

// Sample only 5% of high-volume health checks
app.use("/health", createExpressMiddleware(lowSdk));

L402 / LSAT Support

L402 is a Lightning Network payment protocol that uses the HTTP WWW-Authenticate header to communicate payment requirements.

The SDK automatically detects L402 challenges when the www-authenticate header starts with L402:

WWW-Authenticate: L402 invoice="lnbc...", macaroon="..."

When detected, the SDK extracts:

  • paymentAddress — the Lightning invoice from the invoice parameter
  • paymentNetwork — set to "lightning" automatically

No configuration is required. If you have already added www-authenticate to redaction.allowedHeaders for another reason, the L402 parsing still works because the header is parsed before redaction is applied.


Custom x402 Field Mapping

If your API uses non-standard header names for payment metadata, configure the field mapping:

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-network",
      token:   "x-ln-currency",
      status:  "x-payment-result",
    },
  },
});

Only override the fields that differ from the defaults. Unspecified fields continue to use x-payment-{field}.

Reading from the response body

Set source: "body" or "both" and attach the parsed body to the framework-specific property before responding:

Express:

app.get("/api/paid", (_req, res) => {
  const body = {
    "x-payment-address": "lnbc...",
    "x-payment-amount": "5000",
    "x-payment-network": "lightning",
    "x-payment-token": "SATS",
  };

  res.locals.x402Body = body;     // attach for SDK
  res.status(402).json(body);
});

Fastify:

app.get("/api/paid", async (request, reply) => {
  const body = { "x-payment-address": "lnbc...", "x-payment-amount": "5000" };
  request.x402Body = body;        // attach for SDK
  return reply.status(402).send(body);
});

When source: "both", header values take precedence over body values for the same field.


Building a Custom Adapter

If you use a framework not supported by the built-in adapters, you can build your own using the SDK's low-level API:

import {
  createLedgergateSdk,
  createRequestContext,
  captureResponseData,
  shouldSample,
  buildRequestReceivedEvent,
  buildRequestCompletedEvent,
  detectX402,
} from "@ledgergate/ledgergate-sdk";

const sdk = createLedgergateSdk({ apiKey: "..." });

// In your framework's middleware / hook:
function myMiddleware(req, res, next) {
  const sampled = shouldSample(sdk.config.sampleRate);

  const context = createRequestContext({
    method: req.method,
    path: req.path,
    headers: req.headers,
    remoteAddress: req.ip,
    redaction: sdk.config.redaction,
    sampled,
  });

  if (sampled) {
    sdk.queue.enqueue(buildRequestReceivedEvent(context));
  }

  // Hook into response completion (framework-specific)
  onResponseFinish(res, (statusCode, responseHeaders) => {
    if (!sampled) return;

    const responseData = captureResponseData(context, statusCode);
    const x402 = detectX402(statusCode, responseHeaders, sdk.config.x402);

    if (x402) {
      // Use buildPaymentRequiredEvent / buildPaymentVerifiedEvent / etc.
    } else {
      sdk.queue.enqueue(buildRequestCompletedEvent(context, responseData));
    }
  });

  next();
}

All low-level exports are documented in the SDK exports table below.


Debug Mode

Enable debug logging to trace SDK internals:

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

In debug mode, the SDK logs to console:

  • Each event enqueued and current buffer size
  • Each batch flush (event count and endpoint)
  • Success or failure of each HTTP send
  • Retry attempts with backoff delays
  • Shutdown lifecycle messages

SDK Exports

The full public API surface of @ledgergate/ledgergate-sdk:

Factory

ExportDescription
createLedgergateSdk(config)Creates and returns an SdkInstance

Adapters

ExportDescription
createExpressMiddleware(sdk)Express RequestHandler
fastifyLedgergateFastify plugin
FastifyLedgergateOptions{ sdk: SdkInstance } type

Core

ExportDescription
createRequestContext(options)Builds an immutable RequestContext
captureResponseData(context, statusCode)Returns ResponseData with latency
shouldSample(rate)Returns true if this request should be tracked
createTimer()High-resolution timer (used internally)
getTimestamp()Returns current ISO 8601 timestamp string

Privacy

ExportDescription
extractClientIp(headers, directIp?)Best-guess client IP from headers
hashIp(ip, salt?)SHA-256 hash, truncated to 16 chars
isSensitiveHeader(name)true if header should be redacted
redactHeaders(headers, allowlist?)Returns new redacted headers object

Events

ExportDescription
buildRequestReceivedEvent(context)
buildPaymentRequiredEvent(context, payment, response)
buildPaymentVerifiedEvent(context, payment, response)
buildPaymentFailedEvent(context, payment, response)
buildRequestCompletedEvent(context, response, payment?)
AnalyticsEventSchemaZod validation schema
EventTypeEnum of event type strings
PaymentStatusEnum of payment status strings

x402

ExportDescription
detectX402(statusCode, headers, config?, body?)Returns X402Metadata or undefined
isPaymentRequired(statusCode)true if status is 402
parsePaymentHeaders(headers, fieldMapping?)Extract payment fields from headers
parsePaymentBody(body, fieldMapping?)Extract payment fields from JSON body
applyX402DetectionDefaults(input?)Merge user config with defaults

Config

ExportDescription
parseConfig(input)Validates and returns SdkConfig (throws on invalid)
safeParseConfig(input)Zod safe-parse variant
SdkConfigSchemaZod schema for the full config
RedactionConfigSchemaZod schema for the redaction sub-object
TransportConfigSchemaZod schema for the transport sub-object

Types

ExportDescription
SdkConfigFully resolved config interface
SdkConfigInputInput type (before defaults applied)
RedactionConfigResolved redaction config
TransportConfigResolved transport config
SdkInstanceObject returned by createLedgergateSdk()
RequestContextImmutable per-request context
ResponseDataStatus code + latency
AnalyticsEventValidated event object
X402MetadataPayment metadata extracted from a response
X402DetectionConfigResolved x402 detection config
PaymentFieldMappingCustom header/body key mapping
PaymentMetadataSource"header" | "body" | "both"

On this page