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 theinvoiceparameterpaymentNetwork— 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
| Export | Description |
|---|---|
createLedgergateSdk(config) | Creates and returns an SdkInstance |
Adapters
| Export | Description |
|---|---|
createExpressMiddleware(sdk) | Express RequestHandler |
fastifyLedgergate | Fastify plugin |
FastifyLedgergateOptions | { sdk: SdkInstance } type |
Core
| Export | Description |
|---|---|
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
| Export | Description |
|---|---|
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
| Export | Description |
|---|---|
buildRequestReceivedEvent(context) | |
buildPaymentRequiredEvent(context, payment, response) | |
buildPaymentVerifiedEvent(context, payment, response) | |
buildPaymentFailedEvent(context, payment, response) | |
buildRequestCompletedEvent(context, response, payment?) | |
AnalyticsEventSchema | Zod validation schema |
EventType | Enum of event type strings |
PaymentStatus | Enum of payment status strings |
x402
| Export | Description |
|---|---|
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
| Export | Description |
|---|---|
parseConfig(input) | Validates and returns SdkConfig (throws on invalid) |
safeParseConfig(input) | Zod safe-parse variant |
SdkConfigSchema | Zod schema for the full config |
RedactionConfigSchema | Zod schema for the redaction sub-object |
TransportConfigSchema | Zod schema for the transport sub-object |
Types
| Export | Description |
|---|---|
SdkConfig | Fully resolved config interface |
SdkConfigInput | Input type (before defaults applied) |
RedactionConfig | Resolved redaction config |
TransportConfig | Resolved transport config |
SdkInstance | Object returned by createLedgergateSdk() |
RequestContext | Immutable per-request context |
ResponseData | Status code + latency |
AnalyticsEvent | Validated event object |
X402Metadata | Payment metadata extracted from a response |
X402DetectionConfig | Resolved x402 detection config |
PaymentFieldMapping | Custom header/body key mapping |
PaymentMetadataSource | "header" | "body" | "both" |