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

@t-0/usdt-pay-sdk

v0.2.9

Published

SDK for the t-0 USDt pay flow (tzero.v1.pay)

Readme

@t-0/usdt-pay-sdk

TypeScript SDK for the t-0 USDt pay flow — the contract under proto/tzero/v1/pay/. Generated Connect clients and servers for all three roles, over a transport that signs what you send and verifies what arrives.

Start from a scaffolded project rather than from here: usdt-pay init --lang=node --role=issuer or --role=lp (install and every flag: cli/README.md). The project's README is the integration guide: issuer · LP.

Install

npm install @t-0/usdt-pay-sdk

Scaffolding a project with the CLI (usdt-pay init) adds this dependency for you.

Serving the callbacks t-0 pushes to you

import { createServer, IssuerCallbackService } from "@t-0/usdt-pay-sdk";

const server = await createServer(8080, process.env.NETWORK_PUBLIC_KEY!, (r) => {
  r.service(IssuerCallbackService, issuerCallbackHandler);
});

Every inbound request is verified against t-0's public key before it reaches your handler, and every response is validated against the contract's buf.validate constraints on the way out. Verification runs over the bytes that arrived — protobuf encoding is not canonical, so a re-serialized message is a different message to secp256k1, and createServer wires the raw-body hasher in for you.

The returned value is a listening http.Server: close() it to shut down, and read address() when you passed port 0.

Mount one service per role edge you implement — IssuerCallbackService, AcquirerCallbackService, LpCallbackService.

Mounting into a server you already run

createHandler is the same thing as createServer minus the http.Server: a plain (req, res) handler you mount wherever you want — Express, Fastify (via its middleware bridge), or a raw http server you multiplex yourself.

import express from "express";
import { createHandler, IssuerCallbackService } from "@t-0/usdt-pay-sdk";

const app = express();

// Mount BEFORE any body parser — the handler must see the raw bytes.
app.use(
  "/sda/payments/t0",
  createHandler(process.env.NETWORK_PUBLIC_KEY!, (r) => {
    r.service(IssuerCallbackService, issuerCallbackHandler);
  }),
);

app.use(express.json()); // parsers for the rest of the app go after
app.listen(3000);

Two constraints, both about bytes:

  • Mount it before anything that touches the body. The signature is verified against the raw bytes as they stream in; express.json() and friends consume the stream, and anything that decompresses changes the bytes. Order the middleware so the handler is first.
  • Prefix mounting works because Express strips the prefix from req.url before the handler routes on the Connect path (/<package>.<Service>/<Method>). A framework that passes the full URL through needs the handler at the root, or the prefix removed first.

The health service t-0 probes (grpc.health.v1.Health/Check) is inside the handler, behind the same signature check — mounting the handler mounts it.

Standalone request decoding

For frameworks that don't use Node's http.createServer (Hono, Effect, Koa, Fastify, etc.), use createRequestDecoder for one-call signature verification + Content-Type-aware decoding + protovalidation. It returns an either-type result: success with the decoded message and a response encoder, or failure with a ready-to-send HTTP error.

import { create } from "@bufbuild/protobuf";
import { createRequestDecoder } from "@t-0/usdt-pay-sdk/crypto";
import {
  CreatePaymentInstructionsRequestSchema,
  CreatePaymentInstructionsResponse_Failure_Reason,
  CreatePaymentInstructionsResponseSchema,
} from "@t-0/usdt-pay-sdk";

const decode = createRequestDecoder({
  networkPublicKey: process.env.NETWORK_PUBLIC_KEY!, // "0x04..." uncompressed secp256k1
});

// Hono / fetch-shaped framework — route by Connect procedure path:
app.post("/tzero.v1.pay.issuer.IssuerCallbackService/CreatePaymentInstructions", async (c) => {
  const body = new Uint8Array(await c.req.arrayBuffer());
  const result = decode(CreatePaymentInstructionsRequestSchema, { body, headers: c.req.raw.headers });

  if (!result.ok) {
    return new Response(result.error.body, {
      status: result.error.status,
      headers: result.error.headers,
    });
  }

  const response = create(CreatePaymentInstructionsResponseSchema, {
    result: {
      case: "failure",
      value: { reason: CreatePaymentInstructionsResponse_Failure_Reason.ISSUER_UNAVAILABLE },
    },
  });

  // encodeResponse validates + encodes in the matching wire format (JSON or proto)
  const wire = result.encodeResponse(CreatePaymentInstructionsResponseSchema, response);
  return new Response(wire.body, { status: wire.status, headers: wire.headers });
});
// Raw Node http example:
import http from "node:http";
import { create } from "@bufbuild/protobuf";
import { createRequestDecoder } from "@t-0/usdt-pay-sdk/crypto";
import {
  CreatePaymentInstructionsRequestSchema,
  CreatePaymentInstructionsResponse_Failure_Reason,
  CreatePaymentInstructionsResponseSchema,
} from "@t-0/usdt-pay-sdk";

const decode = createRequestDecoder({
  networkPublicKey: process.env.NETWORK_PUBLIC_KEY!, // "0x04..." uncompressed secp256k1
});

http.createServer((req, res) => {
  const chunks: Buffer[] = [];
  req.on("data", (c) => chunks.push(c));
  req.on("end", async () => {
    try {
      const body = Buffer.concat(chunks);
      const result = decode(CreatePaymentInstructionsRequestSchema, { body, headers: req.headers });

      if (!result.ok) {
        res.writeHead(result.error.status, result.error.headers);
        res.end(result.error.body);
        return;
      }

      const response = create(CreatePaymentInstructionsResponseSchema, {
        result: {
          case: "failure",
          value: { reason: CreatePaymentInstructionsResponse_Failure_Reason.ISSUER_UNAVAILABLE },
        },
      });
      const wire = result.encodeResponse(CreatePaymentInstructionsResponseSchema, response);
      res.writeHead(wire.status, wire.headers);
      res.end(wire.body);
    } catch (e) {
      res.writeHead(500, { "content-type": "application/json" });
      res.end(JSON.stringify({ code: "internal", message: String(e) }));
    }
  });
}).listen(3000);

The decoder accepts both fetch Headers and Node's Record<string, string | string[] | undefined>. It normalizes header case internally, detects Content-Type (application/json or application/proto / application/protobuf / application/x-protobuf), and the returned encodeResponse closure responds in the matching format. The pay contract's proto registry is baked in; you don't pass one.

On success (result.ok === true), result.request is the typed message, result.format is 'json' | 'proto', and result.encodeResponse(schema, message) returns a WireResponse in the matching format. On failure (result.ok === false), result.error carries { status, headers, body } ready to send — signature failures return 401, malformed bodies 400, unsupported Content-Type 415, and validation errors 400 with a violations array.

Important constraints for standalone integrations:

  • Raw body bytes only. Pass the exact wire bytes — no body parsers, no auto-decompression, never re-serialized protobuf. Protobuf encoding is not canonical; re-encoding produces different bytes and breaks verification.
  • Health endpoint. t-0 probes /grpc.health.v1.Health/Check on every endpoint. The probe is signed. Standalone integrations must route this path and return a valid health response. See HEALTH_SERVICE.md for the wire contract.
  • DecodeRequestFailure is an open union. New error shapes may be added without a major version bump. Handle unknown failures as generic errors.

The individual building blocks are also exported: createRequestVerifier, rejectRequest, NetworkHeaders, DEFAULT_TOLERANCE_MS, verifySignature, computeDigest, keccak256, parsePublicKey, publicKeysEqual. You can import them from the ./crypto subpath: import { createRequestVerifier } from "@t-0/usdt-pay-sdk/crypto".

Calling t-0

import { createClient, IssuerService } from "@t-0/usdt-pay-sdk";

const t0 = createClient(process.env.TZERO_ENDPOINT!, privateKeyHex, IssuerService);
const response = await t0.paymentReceived(request, { timeoutMs: 10_000 });

All 14 endpoints are unary request/response — nothing in this contract streams.

endpoint is required: the underlying provider client defaults to a different t-0 API, and a pay participant that omitted it would sign perfectly valid requests and send them to the wrong host.

Deadlines are per call, as { timeoutMs }. A Connect timeout is a duration evaluated when the call is made, so each call site picks its own — the starters give a settlement report more room than the rest, because the transfer is already broadcast by then and an answer is worth waiting for.

signer takes a hex private key, or a SignerFunction when the key lives in an HSM or KMS and never reaches this process.

The public key t-0 knows you by

import { publicKeyFromPrivateKey } from "@t-0/usdt-pay-sdk";

console.log(publicKeyFromPrivateKey(process.env.PROVIDER_PRIVATE_KEY!));

Send it to the t-0 team — that is step 1 of every role's integration. Calling it at startup also fails a malformed key there rather than on the first request.

Generated code

src/gen/ is checked in, so neither the starters nor a consumer needs buf installed. Regenerate it after a proto sync:

npm run buf:generate

buf.gen.yaml pins the protoc-gen-es version; bump it and the @bufbuild/protobuf range in package.json together, so generated code and runtime stay on one major.

Build

npm install     # from node/
npm run build   # ESM to lib/esm, CommonJS to lib/cjs

The package exports both, so it works from an import and from a require.