@zentpay/x402-pay
v0.2.12
Published
x402 payment SDK focused on gateway, payment session, recovery, and worker facilitator flows.
Maintainers
Readme
ZentPay SDK
@zentpay/x402-pay is the TypeScript SDK used by ZentPay Portal, the payment
service, and developer integrations.
For public integration docs, see https://docs.zentpay.app.
Current Product Principle
ZentPay is built around one core payment experience:
One authorization, purchases from 0.01 USDC, and no gas required from the user.
Current product path:
Email login or wallet login
-> The user has USDC in their wallet
-> The user signs one budget authorization
-> The platform or relayer pays gas to relay or register that authorization
-> The user makes repeated small purchases
-> The backend charges USDC to the developer pay_to address through BudgetSpenderV2
-> ZentPay records charges and deliveries
-> The game or app fulfills the purchaseThe current public SDK docs and examples describe the USDC Permit + BudgetSpender path.
Package Surfaces
import { createPaymentSession } from "@zentpay/x402-pay/client";
import { createInjectedWalletAdapter } from "@zentpay/x402-pay/wallet";
import { createPrivyEmbeddedWalletAdapter } from "@zentpay/x402-pay/wallet/privy";
import { zentpayExpress } from "@zentpay/x402-pay/server";Main exports:
client: browser payment sessions, x402 payloads, USDC balance checks, BudgetSpender allowance checks, and permit helpers.wallet: wallet adapter types and utilities.wallet/injected: browser extension wallet adapter.wallet/privy: Privy email embedded wallet adapter.server: Express gateway helpers, Nakama fulfillment helpers, and Hosted ZentPay webhook and delivery helpers.worker: Cloudflare Worker facilitator helpers.
The current server webhook, delivery, account handoff, trusted-server charge,
and zentpay config CLI helpers require @zentpay/x402-pay 0.2.12 or newer.
The published 0.2.11 package does not include createTrustedServerCharge:
npm install @zentpay/x402-pay@^0.2.12The package also ships the zentpay CLI for Config-as-Code workflows:
npx zentpay init next --app gemix --env .env.local
npx zentpay config init --app gemix --file zentpay.config.json
npx zentpay auth browser --app gemix --scopes apps:read,apps:write,products:write,webhooks:write,runtime:write,keys:create --ttl 2h
npx zentpay config plan --file zentpay.config.json --mode merge --json
npx zentpay config apply --file zentpay.config.json --mode merge --yes --json
npx zentpay product create --app gemix --name "AI Usage" --price "$0.01" --pricing dynamic_order --min-amount "$0.01" --max-amount "$5.00"
npx zentpay order create --product zprod_... --amount "$0.37" --idempotency usage:user_123:req_456 --developer-user user_123 --json
npx zentpay price "$0.01"Wallet Adapters
Privy Email Wallet
Use this adapter for low-friction email login.
Capabilities:
- Prepare Privy email login.
- Send an OTP.
- Log in with an OTP.
- Restore an authenticated Privy user.
- Create a signer from the embedded wallet provider.
Privy CAPTCHA is managed in the Privy Dashboard. Do not pass your own
Cloudflare Turnstile site key through .env; the adapter discovers the Privy
public CAPTCHA site key and runs the invisible challenge internally before
sendCode.
Browser Wallet
Use this adapter for users who already have a wallet and USDC.
Supported wallets can sign USDC Permit typed data. A plain approve() call is
only a development diagnostic path because it requires the user to hold ETH for
gas. It is not the normal user path.
BudgetSpender Client Helpers
Read BudgetSpender allowance:
const allowance = await session.getBudgetSpenderAllowance({
spender: "0xb524F0b6b5e35d8b4c24455fcab0390c20Be0324",
});Sign a USDC permit:
const result = await session.signBudgetSpenderPermit({
spender: "0xb524F0b6b5e35d8b4c24455fcab0390c20Be0324",
token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
value: 1_000_000n,
deadline: Math.floor(Date.now() / 1000) + 3600,
});The active Base Sepolia spender is tracked in the deployment notes in
../contracts/README.md.
Server And Fulfillment
The server owns sensitive payment operations:
- Submit
USDC.permit(...). - Call
BudgetSpenderV2.charge(payer, recipient, amount, chargeId). - Pay gas with the relayer or operator key.
- Record authorizations, charges, deliveries, attempts, settlements, and webhooks in durable platform storage.
- Trigger idempotent fulfillment after budget reservation or on-chain settlement.
For Nakama or other game backends, keep payment logic in the payment service. After ZentPay creates a durable delivery record, the payment service can call the game backend to fulfill the purchase.
Hosted ZentPay Helpers
External developer backends can import these helpers from
@zentpay/x402-pay/server:
import {
createTrustedServerCharge,
createZentPayAccountHandoff,
createIdempotencyKey,
reconcileDelivery,
verifyZentPayPaymentProof,
verifyZentPayWebhookRequest,
verifyZentPayWebhookSignature,
} from "@zentpay/x402-pay/server";createZentPayAccountHandoff(): creates a short-lived handoff for the current game or app account. If an active binding and valid budget already exist, it returns a short-livedauthorizationCodeandaccountConnectionIdthat can be used for the next purchase.createIdempotencyKey(): creates a replay-safe key for a frontend purchase intent or backend order.createTrustedServerCharge(): calls backend-onlyPOST /api/chargeswith an app-scopedzpk_...key that hascharges:create. Live products require a product-level trusted max; Dev Console and config-as-code default it to1000000atomic units, or 1 USDC.verifyZentPayPaymentProof(): verifies the signedpaymentProofJWS returned by trusted-server charges before immediate fulfillment. It pins the issuer tohttps://api.zentpay.appby default; passexpectedIssuerorjwksUrlonly for local or test APIs you control. Store proofjti/proofIdor theidempotencyKeyin your fulfillment ledger and reject repeats; signature verification alone is not a replay cache.verifyZentPayWebhookSignature(): verifies a webhook with the raw body,x-zentpay-timestamp, andx-zentpay-signature.verifyZentPayWebhookRequest(): verifies FetchRequestobjects, such as Next.js route handlers.reconcileDelivery(): queries/api/deliveries/:deliveryIdwith a backendzpk_...API key as a fallback when a webhook is delayed.
const handoff = await createZentPayAccountHandoff({
appSlug: "gemix",
merchantUserId: "game-user-123",
gameAccountLabel: "GemixPlayer#1234",
returnUrl: "https://gemix.apps.zentpay.app/zentpay-auth-return?app=gemix",
apiKey: process.env.ZENTPAY_API_KEY!,
});
if (handoff.status === "ready") {
// Use handoff.authorization.authorizationCode for the next product route call.
}For UI state endpoints, return a whitelist instead of the server-only authorization code:
import { toClientSafeAccountHandoff } from "@zentpay/x402-pay/server";
return Response.json(toClientSafeAccountHandoff(handoff));const delivery = await reconcileDelivery({
deliveryId: "ztdlv_...",
apiKey: process.env.ZENTPAY_API_KEY!,
});const charge = await createTrustedServerCharge({
apiKey: process.env.ZENTPAY_API_KEY!,
runtimeOrigin: "https://gemix.apps.zentpay.app",
productSlug: "extra-moves-5",
accountConnectionId: "ztconn_...",
developerUserId: "game-user-123",
idempotencyKey: createIdempotencyKey({
scope: "gemix",
parts: ["game-user-123", "extra-moves-5"],
}),
});
const proof = await verifyZentPayPaymentProof({
paymentProof: charge.paymentProof!,
expected: {
appId: process.env.ZENTPAY_APP_ID!,
productId: process.env.ZENTPAY_PRODUCT_ID!,
deliveryId: charge.deliveryId,
idempotencyKey: charge.idempotencyKey,
accountConnectionId: "ztconn_...",
developerUserId: "game-user-123",
amountAtomic: "30000",
payTo: process.env.ZENTPAY_PAY_TO!,
network: "eip155:84532",
},
});
await grantOnce(proof.deliveryId, proof.idempotencyKey);Only verify proofs returned by ZentPay APIs or passed through a server-side trusted path. Do not accept arbitrary client-supplied proof tokens without pinning the issuer/JWKS and all expected intent fields.
These helpers are server-only. Never bundle ZENTPAY_API_KEY or
ZENTPAY_WEBHOOK_SECRET into browser, game client, or mobile app code.
Portal login is the user's payment identity, not a developer backend. A
real-money app still needs developer backend fulfillment, or a ZentPay-managed
backend when that product is explicitly offered. Account Binding reuses a global
wallet budget only after each app/game account has its own connection.
CLI / Agent Setup
The package also ships a zentpay CLI for developer setup and AI coding-agent
workflows. Prefer browser or device authorization: the developer approves a
short-lived, app-scoped CLI grant from Dev Console, and the agent receives only
that grant.
zentpay auth browser --app gemix --scopes products:write,webhooks:write,keys:create,runtime:write,apps:read --ttl 2h
zentpay auth status --json
zentpay product diff --app gemix --file products.json
zentpay product upsert --app gemix --file products.json --match sku --json --yes
zentpay product create --app gemix --name "AI Usage" --price "$0.01" --pricing dynamic_order --min-amount "$0.01" --max-amount "$5.00"
ZENTPAY_API_KEY=zpk_... zentpay order create --product zprod_... --amount "$0.37" --idempotency usage:user_123:req_456 --developer-user user_123 --json
zentpay webhook upsert --app gemix --url https://game.example.com/api/zentpay/webhook --events payment.delivered
zentpay keys create --app gemix --label "Backend key" --scopes orders:create,orders:read,charges:create,deliveries:read,receipts:read,connections:create,connections:read --env .env.local
zentpay auth revokeCLI grants are not admin tokens and cannot run settlement operations,
recipient allowlist owner work, pay_to review, Portal listing review, or other
ZentPay ops tasks. Backend API keys created by the CLI stay server-side.
auth status verifies the saved grant against the API. config apply --json
returns compact ops, ids, warnings, and one-time secrets by default; pass
--verbose only for a full platform snapshot. keys pull writes comments for
masked key previews, not a usable secret.
Testing Fixtures
Use @zentpay/x402-pay/testing for integration tests without touching the live
API:
import {
createZentPayMockFetch,
createZentPayWebhookFixture,
zentPayHandoffReadyFixture
} from "@zentpay/x402-pay/testing";The fixtures include ready, needs-connection, needs-budget handoff states,
signed payment.delivered webhook payloads, and a mock fetch handler for
handoff, order, delivery, and health routes.
Where x402 Fits
x402 is a good fit for standard HTTP 402 payments. For one authorization and repeated in-app purchases, the core ZentPay path is Account Binding + USDC Permit + BudgetSpender.
Key differences:
exact: one exact payment for one request.upto: a maximum payment for one request, not a reusable game budget.- BudgetSpender: reusable allowance for repeated small charges.
Development
cd sdk
npm run typecheck
npm run test
npm run buildCurrent package name:
@zentpay/x402-payThe current product truth source is ../PRODUCT.md.
