@bitaipay/pay402
v0.1.1
Published
BitAI Pay-402: a tiny SDK for paying (or charging for) an HTTP resource through BitAI Payment's HTTP-402 payment protocol.
Readme
BitAI Pay-402 — Developer Preview
A tiny SDK for paying (or charging for) an HTTP resource through BitAI
Payment, using the standard HTTP 402 Payment Required status code.
No agent-marketplace dependency, no BitAIcoin RPC, no channels — v1
settles instantly over BitAI Payment's own custodial ledger.
If you're integrating an agent that needs to pay for an API, or a service that wants to charge one, this is the whole surface you need. You do not need to understand BitAI Payment's internal services, database, or HTTP framework to use it.
Not Lightning's L402, not the
x402HTTP-USDC scheme. See Comparison to other 402 schemes in the full protocol doc for exactly how and why.
The flow, in one picture
Agent (payer) Service (payee) BitAI Payment
| | |
| 1. GET /reports/q1 | |
|--------------------------------->| |
| | |
| 2. 402 + Challenge | |
|<---------------------------------| |
| | |
| 3. POST /v1/authorizations (signed, as the payer) |
|----------------------------------------------------------------->
| | |
| 4. { proof } (signed by BitAI Payment) |
|<-----------------------------------------------------------------
| | |
| 5. GET /reports/q1 |
| X-Bitai-Pay402-Proof: <proof> | |
|--------------------------------->| |
| | (verifies the proof offline, |
| | no call to BitAI Payment) |
| 6. 200 + the resource | |
|<---------------------------------| |Steps 1–2 and 5–6 are the standard HTTP-402 dance. Steps 3–4 are the one part specific to BitAI Pay-402: an instant, signed payment through BitAI Payment's ledger.
Install
npm install @bitaipay/pay402 # Node 18.17 or newerimport { Pay402Client } from "@bitaipay/pay402/client"; // payer (an agent that pays)
import { Pay402Guard, requirePay402Payment } from "@bitaipay/pay402/server"; // payee (a service that charges)Try it against the public sandbox
A public developer-preview sandbox runs at https://pay402.bitaicoin.com.
Sandbox only.
BAIC_TESThas no value. The sandbox runs on a single server with a single-node database: not highly available, may be reset, not a production service. Do not send it anything you cannot lose.
1. Generate a key pair. Your private key never leaves you; you share only the public key.
// keygen.mjs
import { generateKeyPairSync } from "node:crypto";
const { privateKey } = generateKeyPairSync("ed25519");
const jwk = privateKey.export({ format: "jwk" });
const hex = (b64url) => Buffer.from(b64url, "base64url").toString("hex");
console.log("PRIVATE_KEY_HEX=" + hex(jwk.d)); // secret: keep it
console.log("PUBLIC_KEY_HEX=" + hex(jwk.x)); // send this one2. Get registered. Sandbox access is operator-managed for now (there is deliberately no
public registration endpoint). Send the maintainer a principal id (3-40 characters: lowercase
letters, digits, hyphens) and your PUBLIC_KEY_HEX, once for a payer and once for a payee
if you are also running a service. A payer is credited a small BAIC_TEST balance.
3. Run a paid request. A payee service and a payer, using only the documented API:
// service.js (payee: charges 0.01 BAIC_TEST for /reports/:quarter)
import Fastify from "fastify";
import { Pay402Guard, requirePay402Payment } from "@bitaipay/pay402/server";
const app = Fastify();
const guard = new Pay402Guard({
payeePrincipalId: process.env.PAYEE_PRINCIPAL_ID,
bitaiPaymentUrl: "https://pay402.bitaicoin.com",
});
app.get(
"/reports/:quarter",
{
preHandler: requirePay402Payment(guard, (request) => ({
resourceRef: `GET /reports/${request.params.quarter}`,
amountSatoshis: 1_000_000n,
})),
},
async (request, reply) => {
reply.send({ quarter: request.params.quarter, revenue: "$1,234,567" });
},
);
await app.listen({ port: Number(process.env.PORT ?? 3000), host: "127.0.0.1" });
console.log("READY");// payer.js (payer: pays the 402 and prints the resource)
import { Pay402Client, Pay402ClientError } from "@bitaipay/pay402/client";
const client = new Pay402Client({
principalId: process.env.PAYER_PRINCIPAL_ID,
privateKey: Buffer.from(process.env.PAYER_PRIVATE_KEY_HEX, "hex"),
});
try {
const result = await client.fetchProtected(`${process.env.SERVICE_URL}/reports/q1`);
console.log(JSON.stringify({ ok: true, paid: result.paid, body: await result.response.json() }));
} catch (error) {
if (error instanceof Pay402ClientError) {
console.log(JSON.stringify({ ok: false, code: error.code, message: error.message }));
process.exit(1);
}
throw error;
}npm install @bitaipay/pay402 fastify
PAYEE_PRINCIPAL_ID=<your-payee-id> node service.js # terminal 1
SERVICE_URL=http://127.0.0.1:3000 PAYER_PRINCIPAL_ID=<your-payer-id> \
PAYER_PRIVATE_KEY_HEX=<your-private-key> node payer.js # terminal 2An unfunded payer gets INSUFFICIENT_BALANCE. That is expected, and is handled below.
10-minute integration example
If you're the payer (an agent that needs to pay for an API)
import { Pay402Client } from "@bitaipay/pay402/client";
const client = new Pay402Client({
principalId: "agent-abc123", // your INDIVIDUAL principal id, provisioned with BitAI Payment
privateKey: myEd25519PrivateKey, // the key that principal was registered with
});
const result = await client.fetchProtected("https://api.example.com/reports/q1");
if (result.response.ok) {
const report = await result.response.json();
}That's it. If the resource costs nothing (no 402), fetchProtected
behaves exactly like fetch. If it costs something, the SDK pays it,
verifies BitAI Payment's proof, and retries automatically.
Handle payment-specific failures with Pay402ClientError:
import { Pay402Client, Pay402ClientError } from "@bitaipay/pay402/client";
try {
await client.fetchProtected(url);
} catch (error) {
if (error instanceof Pay402ClientError) {
switch (error.code) {
case "INSUFFICIENT_BALANCE":
// top up and retry
break;
case "CHALLENGE_EXPIRED":
// the 402 was stale; request the resource again for a fresh one
break;
case "ALREADY_PAID_BY_OTHER":
// a race with another payer for the same reference -- rare, safe to retry with a fresh request
break;
default:
throw error;
}
} else {
throw error;
}
}See Pay402ClientErrorCode for
the full list.
If you're the payee (a service that wants to charge for an endpoint)
Using Fastify:
import { Pay402Guard, requirePay402Payment } from "@bitaipay/pay402/server";
const guard = new Pay402Guard({
payeePrincipalId: "service-xyz789", // your INDIVIDUAL principal id
bitaiPaymentUrl: "https://pay402.bitaicoin.com", // the sandbox; use your own deployment later
});
app.get(
"/reports/:quarter",
{
preHandler: requirePay402Payment(guard, (request) => ({
resourceRef: `GET /reports/${request.params.quarter}`,
amountSatoshis: 1_000_000n, // 0.01 BAIC_TEST
})),
},
async (request, reply) => {
// Only reached once a valid, unredeemed proof was presented.
reply.send({ quarter: request.params.quarter, revenue: "$1,234,567" });
},
);Pay402Guard itself has no Fastify dependency — requirePay402Payment
is a ~20-line adapter (sdk/server/fastify-pay402.ts) you can copy and
rewrite for Express, Koa, or a raw http.Server in a few minutes; see
Framework independence.
Run the reference application (from a clone of the repo)
pnpm exec tsx sdk/reference-app/run.tsBoots a real BitAI Payment instance, a real reference resource server, and a real payer agent, all talking real HTTP, and prints every step. No external services required (PGlite in-process; BitAI Pay-402 never touches BitAIcoin at all).
Where to go next
docs/PROTOCOL.md— the full spec: Challenge and Proof schemas, trust model, security/replay model, and how this differs from Lightning's L402 and thex402HTTP-USDC scheme.client/— the payer-side SDK.server/— the payee-side SDK (framework-agnostic core + a Fastify adapter).reference-app/— the minimal working example above, in full.test/pay402-sdk.test.ts— the SDK's own hard acceptance test, covering every failure mode listed above against real HTTP servers.
