Deploy
The merchant SDK is a regular Node package — no native deps, no persistent state, no webhooks. Anywhere Node runs, this runs.
Recommended platforms
- Railway, Fly, Render. Drop in with a
Dockerfileor a buildpack. SetMERCHANT_ETA_ADDRESS+HELIUS_RPC_URLas env vars. Best fit for Express-style servers. - Vercel / Cloudflare Workers. Use the SDK inside a Next.js route
handler or a Workers fetch handler — the
charge()middleware has no Express runtime dep, just express-shaped req/res. Express adapter helpers are on the roadmap. - AWS Lambda / GCF. Wrap with
serverless-http. Cold-start cost is ~300ms for the first request's RPC handshake; warm requests are ~150–400ms (twogetTransactionreads).
Environment variables
| Variable | Required | Notes |
|---|---|---|
MERCHANT_ETA_ADDRESS | Yes | Copy from your merchant dashboard. The Solana pubkey of your Umbra encrypted token account. |
HELIUS_RPC_URL | Recommended | Production-grade RPC endpoint. Default is the public Solana RPC, which is rate-limited. helius.dev is generous on the free tier. |
There is no facilitator API key — verification is direct chain reads.
Scaling notes
- Stateless on the request path. No DB, no caches besides the in-memory replay map (queueSignature → seen-at).
- One RPC dependency. Two
getTransactioncalls per paid request. At 100 RPS that's 200 RPC reads/sec — well within Helius free-tier limits, comfortable on any paid tier. - Replay map is per-process. Horizontal scale across multiple Node processes means each holds its own map. For correctness this is fine — the on-chain queue tx is the source of truth, and a duplicate retry that happens to hit a different process still fails the on-chain check (queue tx already consumed). For tightness against short-window replays across processes, back the map with Redis.
Health checks
The middleware doesn't interfere with /healthz, /_ping, or
whatever pattern your platform uses — it only runs on routes you
explicitly wrap with pay.charge().
ts
app.get("/healthz", (_req, res) => res.json({ ok: true })); // free
app.get("/article/:id", pay.charge({ amount: "10000" }), handler); // paidNext: Error handling.