npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@txnod/sdk

v1.0.2

Published

TypeScript SDK for the txnod non-custodial crypto payment gateway — sign requests, verify webhook signatures, and verify invoice deposit-address derivation across BTC, ETH, TRON, Cardano, Polygon PoS, BNB Smart Chain, and TON. Pure Node >= 20; runs in any

Readme

@txnod/sdk

Fully-typed REST client and webhook verifier for TxNod — a non-custodial crypto payment gateway. Zero cryptocurrency runtime dependencies, automatic HMAC signing on every API call, RFC 7807 → typed error mapping covering every server error_code, and a verifyWebhookSignature helper with a ±300-second timestamp window. Pure Node ≥ 20 — runs unchanged in Express, Fastify, Hono, Koa, NestJS, plain node:http, Next.js (App Router or Pages), Nuxt, SvelteKit, Remix, Astro endpoints, or any other server-side framework. Production and testnet are both supported end-to-end, including SDK-side address verification — pick at project-create time, never per-invoice.

Install

npm install @txnod/sdk
pnpm add @txnod/sdk
yarn add @txnod/sdk
bun add @txnod/sdk

Node ≥ 20 is required. Zero runtime and zero peer dependencies — @txnod/sdk is the only package you install. Types are fully expanded to plain TypeScript in the published tarball, so the SDK is compatible with any zod version your project uses (or no zod at all).

Environment setup

# .env.local
TXNOD_PROJECT_ID=01JXXXXXXXXXXXXXXXXXXXXXXX
TXNOD_API_SECRET=<64-hex-character API secret>
TXNOD_WEBHOOK_SECRET=<same as TXNOD_API_SECRET — see docs>

# Optional — defaults to https://txnod.com.
# TXNOD_BASE_URL=https://pay.mycompany.com

The operator generates the project id and API secret in the TxNod dashboard (API Keys → Generate). The secret is shown exactly once — copy it into your secrets manager immediately. The webhook secret is the same value as the API secret; the dashboard surfaces both names so partners can wire two distinct env vars without re-deriving the relationship.

TXNOD_BASE_URL is only needed when targeting a non-default origin — a staging deployment, or a self-hosted txnod-compatible gateway. The SDK's HMAC scheme, retry logic, and webhook verifier are endpoint-agnostic.

Testing without a real wallet — sandbox mode

Sandbox projects work end-to-end without on-chain interaction: create one in the dashboard, drive client.sandbox.simulate* to walk the state machine, and your webhook handler receives real signed events with mode: 'sandbox'. The SDK surface is documented in docs/05-sandbox.md (bundled in this tarball at node_modules/@txnod/sdk/docs/05-sandbox.md); an end-to-end Vitest harness covering all seven webhook event types is at docs/examples/sandbox-vitest-suite.md.

Supported coins

Operators enable coins per-project in the TxNod dashboard; a single TxnodClient instance can create invoices for any enabled coin — no SDK-level coin-capability toggle. The coin value passed to createInvoice must be one of the 15 literals below.

| Coin enum | Chain | Description | |---|---|---| | btc | btc | Bitcoin native (BTC). | | eth | eth | Ethereum native (ETH). | | usdt_erc20 | eth | Tether USD, ERC-20 on Ethereum. | | usdc_erc20 | eth | USD Coin, ERC-20 on Ethereum. | | trx | tron | TRON native (TRX). | | usdt_trc20 | tron | Tether USD, TRC-20 on TRON. | | ada | ada | Cardano native (ADA). | | pol | polygon | Polygon PoS native (POL). | | usdt_polygon | polygon | Tether USD, ERC-20 on Polygon PoS. | | usdc_polygon | polygon | USD Coin, ERC-20 on Polygon PoS. | | bnb | bsc | BNB Smart Chain native (BNB). | | usdt_bep20 | bsc | Tether USD, BEP-20 on BNB Smart Chain. | | usdc_bep20 | bsc | USD Coin, BEP-20 on BNB Smart Chain. | | ton | ton | TON native (Toncoin). | | usdt_ton | ton | Tether USD jetton on TON. |

SDK-side address verification (TXNOD_<chain>_XPUB)

When the matching env var is set, createInvoice automatically derives the expected deposit address from the operator's xpub at the invoice's derivation_path and rejects with AddressVerificationError on mismatch. This is the SDK's defense against a compromised gateway silently swapping the deposit address. Set per chain you accept; omit to skip verification on that chain.

| Chain | Env var | Format | |---|---|---| | BTC | TXNOD_BTC_XPUB | zpub (mainnet) / vpub/tpub (testnet) | | ETH + ERC-20 | TXNOD_ETH_XPUB | xpub (mainnet) / tpub (testnet) | | TRON | TXNOD_TRON_XPUB | xpub / tpub | | Cardano | TXNOD_CARDANO_ACCOUNT_PUBKEY | CIP-5 acct_xvk bech32 | | Polygon PoS | TXNOD_POLYGON_XPUB | xpub / tpub | | BNB Smart Chain | TXNOD_BNB_XPUB | xpub / tpub | | TON | TXNOD_TON_PUBKEY (+ siblings) | 64-hex Ed25519 pubkey — see below |

Hardware-wallet users (Ledger or Trezor): ETH, Polygon PoS, and BNB Smart Chain share BIP-44 coin_type 60. One device account → one xpub → set the same value on all three env vars (TXNOD_ETH_XPUB, TXNOD_POLYGON_XPUB, TXNOD_BNB_XPUB). The wizard's "EVM" hint mirrors this; the SDK's per-chain address derivation differs in chain-id only. (TRON and TON are Ledger-only on the hardware-wallet side — Trezor doesn't speak either chain. Cardano via Trezor requires Model T or newer.)

Each xpub env var accepts three formats:

  • Single xpub — the most common case: TXNOD_BTC_XPUB=zpub6r….
  • CSV (newest-first) — for wallet rotations where multiple xpubs must be accepted in parallel: TXNOD_BTC_XPUB=zpub6_new,zpub6_old. Verification iterates newest-first and accepts on the first match; an AddressVerificationError is raised only when every configured xpub fails to derive the address.
  • JSON array — equivalent to CSV when you prefer explicit shape: TXNOD_BTC_XPUB=["zpub6_new","zpub6_old"].

For TON the operator's wallet has no HD derivation — the address is fully determined by (walletPublicKey, walletVersion, subwalletId, workchain). Configure via four env vars (defaults match a stock Ledger TON setup):

| Env var | Required | Default | |---|---|---| | TXNOD_TON_PUBKEY | yes (enables auto-verify) | — | | TXNOD_TON_WALLET_VERSION | no | v4R2 (allowed: v3R2, v4R2, v5R1) | | TXNOD_TON_SUBWALLET_ID | no | 698983191 (standard for v3R2/v4R2) | | TXNOD_TON_WORKCHAIN | no | 0 (basechain; allowed: 0, -1) |

The pubkey is the 32-byte Ed25519 public key the operator's wallet device exports — the same value Ledger Live, Tonkeeper, etc. expose under "show recovery key" / "wallet pubkey". Never the secret key. When TXNOD_TON_PUBKEY is unset, TON verification is skipped silently — the SDK will emit one warn line per unverified TON invoice so operators do not silently coast on server trust.

Wallet rotations: after changing the env value in a long-lived process, call client.refreshXpubConfig() to pick up the new values without restarting.

TRON requires per-address activation

TRON is the only chain in the list that charges the recipient to "activate" each fresh address before it can hold balance. TxNod's operator dashboard handles activation on the operator side; partners do nothing extra. The only partner-visible artifact is a single typed error: when an operator's TRON address pool has zero activated rows at invoice creation time, createInvoice({ coin: 'trx' | 'usdt_trc20' }) raises TxnodTronNoActivatedAddressesError. The error's walletId field carries the operator wallet id — surface a "your operator has not finished TRON wallet setup" message and do not auto-retry; the operator must activate addresses through the dashboard before TRON invoices succeed. The canonical handler example is in docs/02-invoices.md (bundled in this tarball).

TON: payment-token matching

TON is a memo chain — every TON invoice shares the operator's single deposit address and is disambiguated by an 8-hex payment_token placed in the on-chain transaction comment. createInvoice({ coin: 'ton' | 'usdt_ton' }) returns the token alongside the deposit address:

import { TxnodClient } from '@txnod/sdk';

declare const client: TxnodClient;
// `qrcode` is partner-installed; the SDK does not depend on it.
declare const qrcode: { toDataURL: (input: string) => Promise<string> };

const invoice = await client.createInvoice({
  amount_usd: 5,
  coin: 'ton',
  external_id: 'order-ton-1',
});
// invoice.payment_token === 'a1b2c3d4'  (8 lowercase hex chars)
// invoice.payment_uri   === 'ton://transfer/UQ.../?amount=5000000000&text=a1b2c3d4'

// Render the URI as a QR code on the checkout page:
const dataUrl: string = await qrcode.toDataURL(invoice.payment_uri);

The wallet apps (Tonkeeper, MyTonWallet, Ledger Live) auto-fill the payment_token into the comment field when the user follows the URI or scans the QR — no manual paste required. If the user types the address into a wallet by hand and forgets the comment, the deposit lands on the operator's wallet but cannot be matched to the invoice. Such deposits surface in the operator dashboard's "Unattributed deposits" view rather than as invoice.paid webhooks. Surface a "scan the QR or copy the entire payment_uri" instruction at checkout to keep this rare. For HD chains (BTC / ETH / TRON / ADA / Polygon / BSC), invoice.payment_token is always null.

Quickstart — create an invoice

A complete app/api/checkout/route.ts against the Next.js App Router:

import { randomUUID } from 'node:crypto';
import { TxnodClient, TxnodCoinNotEnabledError, TxnodError } from '@txnod/sdk';

const client = new TxnodClient({
  projectId: process.env.TXNOD_PROJECT_ID!,
  apiSecret: process.env.TXNOD_API_SECRET!,
  // baseUrl defaults to 'https://txnod.com'. Override for a self-hosted or
  // staging deployment (see TXNOD_BASE_URL above):
  ...(process.env.TXNOD_BASE_URL ? { baseUrl: process.env.TXNOD_BASE_URL } : {}),
});

export async function POST(request: Request): Promise<Response> {
  try {
    const invoice = await client.createInvoice({
      amount_usd: 9.99,
      coin: 'usdt_trc20',
      external_id: randomUUID(),
      callback_url: new URL('/api/txnod-webhook', request.url).toString(),
    });
    return Response.json({
      invoice_id: invoice.id,
      pay_to: invoice.address,
      amount_crypto: invoice.amount_crypto,
      payment_uri: invoice.payment_uri,
      expires_at: invoice.expires_at,
    });
  } catch (err) {
    if (err instanceof TxnodCoinNotEnabledError) {
      return Response.json(
        { error: 'coin_not_enabled', request_id: err.request_id },
        { status: 422 },
      );
    }
    if (err instanceof TxnodError) {
      return Response.json(
        { error: err.error_code, request_id: err.request_id },
        { status: err.status },
      );
    }
    throw err;
  }
}

Quickstart — verify a webhook

A complete app/api/txnod-webhook/route.ts that validates the inbound HMAC and narrows the typed event:

import {
  verifyWebhookSignature,
  TxnodHmacError,
  TxnodTimestampError,
} from '@txnod/sdk';

export async function POST(request: Request): Promise<Response> {
  const rawBody = await request.text();
  try {
    const event = verifyWebhookSignature(
      request.headers,
      rawBody,
      process.env.TXNOD_WEBHOOK_SECRET!,
    );
    if (event.event_type === 'invoice.paid') {
      // event.data is fully typed under the `invoice.paid` discriminant.
      console.log('paid invoice', event.data.invoice_id, 'at', event.created_at_iso);
    }
    return Response.json({ ok: true });
  } catch (err) {
    if (err instanceof TxnodHmacError) {
      return new Response('bad signature', { status: 401 });
    }
    if (err instanceof TxnodTimestampError) {
      console.warn('clock skew', err.skew_seconds);
      return new Response('stale signature', { status: 401 });
    }
    throw err;
  }
}

Polygon PoS — create and verify (usdt_polygon)

Polygon PoS invoices settle after 128 blocks — the Heimdall checkpoint depth at which the SDK marks an invoice.paid event terminal. The snippet below creates a USDT-Polygon invoice and, in the webhook route, matches the inbound invoice.paid event against the invoice id.

import { randomUUID } from 'node:crypto';
import { TxnodClient, verifyWebhookSignature, TxnodError } from '@txnod/sdk';

const client = new TxnodClient({
  projectId: process.env.TXNOD_PROJECT_ID!,
  apiSecret: process.env.TXNOD_API_SECRET!,
});

const invoice = await client.createInvoice({
  amount_usd: 25.0,
  coin: 'usdt_polygon',
  external_id: randomUUID(),
  callback_url: 'https://my-site.com/api/txnod-webhook',
});

export async function POST(request: Request): Promise<Response> {
  try {
    const event = verifyWebhookSignature(request.headers, await request.text(), process.env.TXNOD_WEBHOOK_SECRET!);
    if (event.event_type === 'invoice.paid' && event.data.invoice_id === invoice.id) {
      console.log('Polygon USDT payment confirmed after 128 blocks.');
    }
    return Response.json({ ok: true });
  } catch (err) {
    if (err instanceof TxnodError) console.error(err.error_code, err.request_id);
    throw err;
  }
}

BNB Smart Chain — create and verify (usdc_bep20)

BNB Smart Chain invoices settle after 15 blocks (BEP-126 fast-finality plus the Binance deposit convention). Note the 18-decimal trap: usdt_bep20 and usdc_bep20 use 18 decimals on-chain, unlike their 6-decimal ETH / Polygon / TRON counterparts — invoice.amount_crypto already reflects this, so render it verbatim rather than re-scaling it.

import { randomUUID } from 'node:crypto';
import { TxnodClient, verifyWebhookSignature, TxnodError } from '@txnod/sdk';

const client = new TxnodClient({
  projectId: process.env.TXNOD_PROJECT_ID!,
  apiSecret: process.env.TXNOD_API_SECRET!,
});

const invoice = await client.createInvoice({
  amount_usd: 25.0,
  coin: 'usdc_bep20',
  external_id: randomUUID(),
  callback_url: 'https://my-site.com/api/txnod-webhook',
});

export async function POST(request: Request): Promise<Response> {
  try {
    const event = verifyWebhookSignature(request.headers, await request.text(), process.env.TXNOD_WEBHOOK_SECRET!);
    if (event.event_type === 'invoice.paid' && event.data.invoice_id === invoice.id) {
      console.log('BSC USDC payment confirmed after 15 blocks.');
    }
    return Response.json({ ok: true });
  } catch (err) {
    if (err instanceof TxnodError) console.error(err.error_code, err.request_id);
    throw err;
  }
}

Testnet — staging integrations

The kind discriminator ('production' | 'testnet') is fixed at project create time on the dashboard, not per-invoice. Each project is bound to one kind for its entire lifetime; the SDK call site does not change between production and testnet — it is the project credential (projectId / apiSecret) that selects the chain network. To run a staging integration, create a separate testnet-kind project on the dashboard and use that project's secrets in your staging environment.

import { TxnodClient } from '@txnod/sdk';

// Same SDK code path. The kind is implied by the testnet project credential.
const client = new TxnodClient({
  projectId: process.env.TXNOD_TESTNET_PROJECT_ID!,
  apiSecret: process.env.TXNOD_TESTNET_API_SECRET!,
});

const invoice = await client.createInvoice({
  amount_usd: 5.0,
  coin: 'btc',
  external_id: 'staging-order-1',
  callback_url: 'https://staging.example.com/api/txnod-webhook',
});
// The webhook envelope's `mode` field carries 'testnet' for events
// emitted from a testnet-kind project — branch on it if your handler
// processes both staging and production traffic from one webhook URL.

The matching operator wallet on the dashboard side must be registered with the testnet kind (testnet-prefix xpub: tpub/vpub/zpub for BTC, tpub for ETH/Polygon/BSC/TRON, addr_test1... stake for Cardano). Cross-kind binding (production wallet → testnet project, or vice versa) is rejected by the server with a typed wallet_kind_mismatch (HTTP 422) error. Address verification on the SDK side accepts each kind's prefixes for the matching project — no flag, no separate setup. Use a testnet faucet to fund the deposit address; everything else (webhooks, finalization thresholds, idempotency) behaves the same as production, just on testnet confirmations.

Operator-side errors your code MUST handle

Two classes of failure are caused by the operator's side, not the partner's. They cannot be auto-retried; surface them as a structured "checkout temporarily unavailable" state and notify the operator out-of-band.

| Class | When | Recovery | |---|---|---| | TxnodSubscriptionExpiredError (HTTP 402) | Operator's TxNod subscription is not active — writes (createInvoice, cancelInvoice, attribute) are blocked. Reads keep working | Operator renews via dashboard /billing. Do NOT retry | | TxnodTronNoActivatedAddressesError (HTTP 422) | TRON-only: operator's address pool has zero activated rows | Operator activates from dashboard. Do NOT retry. .walletId field carries the operator wallet id for deep-link UX |

import {
  TxnodClient,
  TxnodSubscriptionExpiredError,
  TxnodTronNoActivatedAddressesError,
} from '@txnod/sdk';

declare const client: TxnodClient;

export async function POST(_request: Request): Promise<Response> {
  try {
    await client.createInvoice({
      external_id: 'order-1',
      coin: 'usdt_trc20',
      amount_usd: 9.99,
    });
    return Response.json({ ok: true });
  } catch (err) {
    if (err instanceof TxnodSubscriptionExpiredError) {
      return Response.json({ error: 'gateway_unavailable_billing' }, { status: 503 });
    }
    if (err instanceof TxnodTronNoActivatedAddressesError) {
      return Response.json(
        { error: 'gateway_unavailable_tron', wallet_id: err.walletId },
        { status: 503 },
      );
    }
    throw err;
  }
}

Client tuning options

import { TxnodClient } from '@txnod/sdk';

const client = new TxnodClient({
  projectId: process.env.TXNOD_PROJECT_ID!,
  apiSecret: process.env.TXNOD_API_SECRET!,
  // Per-attempt HTTP timeout (default 30 s). Aborts via AbortSignal.timeout —
  // protects against a hung upstream that would otherwise stall your request.
  requestTimeoutMs: 15_000,
  // Hard cap on response body size (default 1 MiB). Over-cap responses are
  // surfaced as a typed 502 — the SDK never buffers an unbounded body.
  maxResponseBytes: 256 * 1024,
  // Per-attempt observer. One entry per HTTP call (success and each retry).
  // Errors thrown from the logger are swallowed.
  requestLogger: (e) => {
    console.log('txnod', e.method, e.path, e.status, `${e.durationMs}ms`, e.requestId);
  },
});
console.log(client.lastRequestId);

TXNOD_SDK_LOG_LEVEL controls the SDK's internal pino logger (default warn). Set to info/debug for more verbose internals. The logger redacts xpub and apiSecret keys.

Idempotent invoice creation — createOrGetInvoice

If your checkout flow may retry the same external_id (network glitch, queue at-least-once delivery), createOrGetInvoice collapses the "create-then-conflict-then-search" pattern into one call:

import { TxnodClient } from '@txnod/sdk';

declare const client: TxnodClient;

const invoice = await client.createOrGetInvoice({
  external_id: 'order-42',
  coin: 'usdt_trc20',
  amount_usd: 9.99,
  callback_url: 'https://your-site.com/api/txnod-webhook',
});
// On TxnodExternalIdConflictError, the SDK fetches the pre-existing invoice
// via searchInvoices({ external_id }) and returns it. Same return shape.
console.log(invoice.id);

Use the underlying createInvoice if you need the external_id_conflict error to bubble up — for example to distinguish first-create from retried-create in your audit log.

Error handling

Every method on TxnodClient throws a subclass of TxnodError on a non-2xx response. The base class carries error_code, status, request_id, and the raw RFC 7807 envelope as raw. The SDK ships a typed subclass for every server error_code (full table in docs/reference/errors.md, bundled in this tarball); the four most operationally useful are shown below.

import {
  TxnodAuthInvalidError,
  TxnodError,
  TxnodPoolExhaustedError,
  TxnodRateLimitError,
  TxnodValidationError,
} from '@txnod/sdk';

export async function safeCreateInvoice(create: () => Promise<unknown>): Promise<unknown> {
  try {
    return await create();
  } catch (err) {
    if (
      err instanceof TxnodRateLimitError ||
      err instanceof TxnodPoolExhaustedError
    ) {
      await new Promise((r) => setTimeout(r, err.retry_after_seconds * 1000));
      return await create();
    }
    if (err instanceof TxnodValidationError) {
      console.warn('validation failed', err.raw.errors);
      throw err;
    }
    if (err instanceof TxnodAuthInvalidError) {
      throw new Error('Re-check TXNOD_PROJECT_ID and TXNOD_API_SECRET.');
    }
    if (err instanceof TxnodError) {
      console.error(err.error_code, err.request_id, err.status);
    }
    throw err;
  }
}

The IDE-hover JSDoc on each error class — visible in any TypeScript-aware editor — is the canonical reference. The full error matrix is documented at docs.txnod.com/api.

Links