d402 adds verifiable on-chain payments to HTTP. A server returns a 402
challenge, the client creates the matching dPayment, and the server authorizes
the retried request from its proof.
npm install d402 @d402/ethers ethersUse payable() when the protected handler can be expressed as a Fetch-style
route:
import { JsonRpcProvider, Wallet } from "ethers";
import { createEthersAdapter } from "@d402/ethers";
import { payable } from "d402/server";
const provider = new JsonRpcProvider(process.env.RPC_URL);
// Any ethers Signer works here, including a KMS or custody-backed signer.
// Signer is only needed if the server performs an on-chain action during the request (for example, Once consumption or refunds).
const payee = new Wallet(process.env.PAYEE_PRIVATE_KEY, provider);
const adapter = createEthersAdapter({ provider, signer: payee });
const terms = {
chainId: 100,
payeeAddress: payee.address,
tokenAddress: null,
netAmount: "1000000000000000",
agreement: { id: "monthly-report:v1" },
expiresAtUnixSec: Math.floor(Date.now() / 1000) + 300,
};
export const GET = payable({
adapter,
payment: { settlementWindow: 3600 },
terms,
handler: (_request, payment) =>
Response.json({
report: "protected data",
paymentId: payment.payment.paymentId,
}),
});The request URL is the payment resource by default. terms may also be an
async function when price or agreement data depends on the request.
Use PaymentAuthorizer when your framework or application owns the controller:
import { PaymentAuthorizer } from "d402/server";
const reportPayment = new PaymentAuthorizer({
adapter,
payment: { settlementWindow: 3600 },
terms,
});
export async function getReport(request: Request): Promise<Response> {
const authorization = await reportPayment.authorize(request);
if (authorization.response !== undefined) {
return authorization.response;
}
const report = await loadReport();
return Response.json({
report,
paymentId: authorization.context.payment.paymentId,
});
}authorize() returns a protocol response when the request needs a challenge,
retry, or rejection. It returns context only after authorization succeeds.
Express middleware, Nest guards, and other framework adapters can translate
their native request into a Fetch Request and use this same API.
Routes are reusable by default. Add Once when one payment should authorize at
most one protected operation:
import { Once, payable, paymentActions } from "d402/server";
const actions = paymentActions({
adapter,
payment: {},
});
const download = payable({
adapter,
payment: { settlementWindow: 3600 },
terms,
consumer: Once(actions),
handler: async () =>
Response.json({ downloadUrl: await createDownloadUrl() }),
});Once is an on-chain, at-most-once claim. If work must survive a crash or lost
HTTP response, store the result under context.payment.paymentId.
Use the default server identity when payment terms represent a stable order or invoice. Put the order ID in the terms so retries reconstruct the same payment:
const orderRoute = payable({
adapter,
payment: {},
terms: (request) => {
const orderId = new URL(request.url).pathname.split("/").at(-1);
return {
chainId: 100,
payeeAddress: payee.address,
tokenAddress: null,
netAmount: "1000000000000000",
agreement: { id: `order:${orderId}:v1` },
expiresAtUnixSec: Math.floor(Date.now() / 1000) + 300,
};
},
handler,
});The same payer, order terms, and server identity produce the same payment identity. This is useful for orders, invoices, and reusable entitlements.
Use client identity for independent per-request payments. This is closer to an x402-style flow, where each request or access attempt should create a fresh payment instead of reusing an order identity:
import { Once, payable, paymentActions } from "d402/server";
const actions = paymentActions({
adapter,
payment: {},
});
const independentPaymentRoute = payable({
adapter,
payment: { identifier: "client" },
terms,
consumer: Once(actions),
handler,
});Client identity is separate from single-use access. Use Once when a payment
may authorize only one operation, and use client identity when each payment
attempt should have an independent payment identity. They can be used together
when every new payment should authorize at most one operation.
For a server, agent, or other unattended client, connect an ethers Wallet to
the provider:
import { JsonRpcProvider, Wallet } from "ethers";
import { createD402Client } from "d402/client";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const signer = new Wallet(process.env.PAYER_PRIVATE_KEY, provider);
const client = await createD402Client({ provider, signer });
const response = await client.fetch(
"https://api.example.com/reports/monthly",
);In a browser, use the signer exposed by MetaMask or another EIP-1193 wallet:
import { BrowserProvider } from "ethers";
import { createD402Client } from "d402/client";
const provider = new BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = await provider.getSigner();
const client = await createD402Client({ provider, signer });
const response = await client.fetch(
"https://api.example.com/reports/monthly",
);The client signer is always the payer. Browser wallets prompt the user when d402 sends a payment transaction; ERC-20 payments may also require a separate token approval transaction.
Use client.d402Fetch() when the completed payment attempt must be persisted
for application recovery or later lifecycle decisions.
The most important integration guide is Payment flows. It shows when to use reusable access, single-use access, stable orders, jobs, credits, and deposits.
- ELI5 protocol overview
- Payment flows
- Protocol diagrams
- API reference
- Advanced configuration
- HTTP and framework integration
- Scaling and stateless deployment
- Upcoming features
- Signing modes
- Refunds
- Disputes
- Testing
- Examples
Apache-2.0