openpay-x402-sdk
v0.4.0
Published
Guarded Node.js buyer SDK for OpenPay x402 JPYC resources
Downloads
798
Readme
openpay-x402-sdk
Node.js 20+ SDK for discovering, quoting, and buying OpenPay x402 resources priced in JPYC. It ships as plain ESM and has no build step.
Quick start
npm install openpay-x402-sdkimport { createOpenPayClient } from 'openpay-x402-sdk';
const client = createOpenPayClient({
privateKey: process.env.BUYER_PRIVATE_KEY,
maxPerCallJpyc: '10',
maxSessionJpyc: '100',
maxDailyJpyc: '250',
allowedHosts: 'open-pay.jp',
});
const catalog = await client.discover({ query: 'demo' });
const quote = await client.quote('https://open-pay.jp/api/paid/demo');
if (quote.ok) {
const result = await client.pay(quote.url, { maxTotalJpyc: '2' });
console.log(result.body, client.session);
}discover() and findShops({ q?, limit? }) return the server response unchanged
inside { ok, status, body }. quote() fetches and validates a 402 challenge but
does not need a signer and never pays. pay() requires a signer and serializes
concurrent calls so every call sees the latest session total.
Sell with the SDK
Create a gate with the exact resource URL registered in OpenPay discovery. For
inexpensive content, handle() verifies and settles the payment in one call:
import { createJpycGate } from 'openpay-x402-sdk';
const gate = createJpycGate({
resourceUrl: process.env.MY_RESOURCE_URL,
});
export async function GET(request) {
const payment = await gate.handle(request);
if (payment instanceof Response) return payment;
const response = Response.json({ your: 'paid content' });
response.headers.set(
'X-PAYMENT-RESPONSE',
payment.paymentResponseHeader,
);
return response;
}For an expensive upstream operation, verify first and settle only after the
operation succeeds. If callUpstream() fails, return an error before calling
settle() so the buyer remains uncharged:
export async function GET(request) {
const payment = await gate.verify(request);
if (payment instanceof Response) return payment;
let data;
try {
data = await callUpstream();
} catch {
return Response.json({ error: 'upstream_failed' }, { status: 502 });
}
const settlement = await payment.settle();
if (settlement instanceof Response) return settlement;
const response = Response.json(data);
response.headers.set(
'X-PAYMENT-RESPONSE',
settlement.paymentResponseHeader,
);
return response;
}createJpycGate fetches accepts from /api/discovery and caches it for five
minutes. Until resourceUrl is listed with a non-empty accepts, handle() and
verify() throw; map that bootstrap condition to an HTTP 500 response. Pass
openpayOrigin to use an origin other than https://open-pay.jp.
The copy-paste paywall snippet generated by OpenPay provides the same one-shot
gate; createJpycGate is its importable SDK counterpart with split settlement.
Money guards
| Option | Default | Guard |
|---|---:|---|
| maxPerCallJpyc | 10 | Upper bound for the caller-provided maxTotalJpyc. |
| maxSessionJpyc | 100 | Cumulative cap for successful payments made by this client instance. |
| maxDailyJpyc | Not set | Persistent cumulative cap per signer and UTC calendar date. |
| allowedHosts | open-pay.jp | Comma-separated bare host allowlist. |
| catalogTrust | true | Also allows catalog URLs after the live challenge matches the catalog challenge. |
| discoveryUrl | https://open-pay.jp/api/discovery | Catalog and OpenPay origin used by the client. |
Query string variants of a query-free listed URL are trusted after the same money-field verification. Exact query-bearing catalog entries remain exact-only.
pay(url, { maxTotalJpyc }) always requires maxTotalJpyc. It is the maximum
total—including the resource price and x402 fee—that this individual call is
authorized to pay. It does not disable or raise maxPerCallJpyc or
maxSessionJpyc; every configured limit must allow the payment.
maxDailyJpyc is opt-in. When set, the client stores successful 2xx unlocks in
~/.openpay-x402/spend.json, keyed by the lower-cased signer address and UTC
date. A missing entry starts at zero. A corrupt/unreadable store or a custom
store returning null rejects quotes and payments with daily_spend_unavailable
(fail-closed). Use spendStore to inject another implementation of
{ load(key), save(key, atomicString) }; MAX_DAILY_JPYC is the equivalent
optional setting for the exported environment config readers.
The file store uses best-effort read-modify-write across processes: payments are serialized within one client process, but separate processes can race and lose an increment. Use an atomic shared store when multiple processes share a signer. Persistence runs only after a successful unlock; a save failure cannot change an already completed payment response.
The client also rejects non-JPYC metadata, unsupported networks or schemes, non-OpenPay forwarder splits, amount inconsistencies, resource URL mismatches, and catalog bait-and-switches before requesting a signature.
Signers
Choose exactly one of privateKey, steward, or signer. Supplying more than
one is a startup error. A custom signer has an EVM address and an async
signTypedData(typedData) method.
Steward keeps signing outside the SDK process:
const client = createOpenPayClient({
steward: {
url: process.env.STEWARD_URL,
tenant: process.env.STEWARD_TENANT,
apiKey: process.env.STEWARD_API_KEY,
agentId: process.env.STEWARD_AGENT_ID,
agentAddress: process.env.STEWARD_AGENT_ADDRESS,
signerId: process.env.STEWARD_SIGNER_ID,
signerSecret: process.env.STEWARD_SIGNER_SECRET,
},
});The first Steward signature is verified locally against agentAddress. Steward
API keys, signer secrets, and local private keys are redacted from SDK-generated
errors and are not exposed as client properties.
Security
Payments can be irreversible. Use a dedicated low-balance wallet, keep private
keys and Steward credentials in a secret manager, and never put them in source
code or logs. Set maxTotalJpyc from the amount authorized for the current
operation, not from the wallet balance. Keep conservative per-call and session
limits even when Steward applies an additional signing policy.
client.session returns a new frozen snapshot on every read:
// { spentAtomic: 2000000000000000000n, spentJpyc: '2' }
console.log(client.session);Advanced consumers may import the named payment, guard, signer, catalog, and executor helpers from the package root.
