@ledewire/x402-client
v0.2.0
Published
x402 fetch wrapper for Ledewire — handles ledewire-wallet scheme, buyer auth, and payment-signature construction
Maintainers
Readme
@ledewire/x402-client
Runtime-agnostic x402 fetch wrapper for the Ledewire payment network. Drop in as a replacement for fetch and it automatically handles 402 Payment Required challenges using the ledewire-wallet scheme — no payment code required in your agent or script.
Works in Node.js 18+, Deno, Cloudflare Workers, and any environment with web-standard fetch.
Install
npm install @ledewire/x402-clientQuick Start
import { createLedewireFetch } from '@ledewire/x402-client'
// Create a buyer API key at ledewire.com/settings/api-keys
const fetch = createLedewireFetch({
key: process.env.LEDEWIRE_BUYER_KEY, // e.g. bktst_abc123
secret: process.env.LEDEWIRE_BUYER_SECRET, // 64-char hex, shown once at creation
})
// That's it — payment is handled transparently
const res = await fetch('https://blog.example.com/posts/great-article')
const article = await res.json()The full x402 handshake happens invisibly:
- First request hits the origin →
402 Payment RequiredwithPAYMENT-REQUIREDheader - Client authenticates with buyer API key → gets buyer JWT
- Client constructs
PAYMENT-SIGNATUREwith JWT +contentIdfrom the server's extension block - Retries original request →
200+ content
What happens under the hood
Your code @ledewire/x402-client
───────────────────────── ─────────────────────────────────────────────
fetch('https://origin/post') → GET /post (no payment)
← 402 + PAYMENT-REQUIRED header
→ POST /v1/auth/login/buyer-api-key
← buyer JWT (cached for token lifetime)
→ GET /post + PAYMENT-SIGNATURE header
← 200 + content
res ←─────────────────────────────────────────────────────────────────────Configuration
import { createLedewireFetch } from '@ledewire/x402-client'
import type { LedewireFetchConfig } from '@ledewire/x402-client'
const fetch = createLedewireFetch({
key: string, // Buyer API key (e.g. bktst_abc123)
secret: string, // 64-char hex secret, shown once at creation
// Optional — defaults to https://api.ledewire.com
// Overridden automatically by the server's PAYMENT-REQUIRED extension block
apiBase?: string,
// Optional — provide a custom fetch (e.g. for proxies or testing)
fetch?: typeof globalThis.fetch,
})The apiBase is self-configuring: when the PAYMENT-REQUIRED response includes an
extensions.ledewire-wallet.apiBase field (standard for Ledewire-gated content), that
value takes precedence over any configured apiBase. This means the same client works
for staging and production content without any configuration changes.
Transport adapters
Under the hood, createLedewireFetch is a one-liner built on two primitives that
you can use directly when you need a different HTTP client:
| Export | Source | Purpose |
| ----------------------- | ----------------------------- | --------------------------------------------- |
| LedewirePaymentClient | @ledewire/x402-client | Holds credentials, builds PAYMENT-SIGNATURE |
| wrapFetchWithPayment | @ledewire/x402-client | Wraps any fetch-compatible function |
| wrapAxiosWithPayment | @ledewire/x402-client/axios | Adds an Axios response interceptor |
This mirrors the @x402/fetch / @x402/axios pattern from the x402 ecosystem — the
client is separate from the transport.
Native fetch (default)
createLedewireFetch is the shorthand for this:
import { LedewirePaymentClient, wrapFetchWithPayment } from '@ledewire/x402-client'
const client = new LedewirePaymentClient({ key, secret })
const fetch = wrapFetchWithPayment(globalThis.fetch, client)
const res = await fetch('https://blog.example.com/posts/article')Axios
Install axios then import from the subpath:
npm install axiosimport axios from 'axios'
import { LedewirePaymentClient } from '@ledewire/x402-client'
import { wrapAxiosWithPayment } from '@ledewire/x402-client/axios'
const client = new LedewirePaymentClient({ key, secret })
const api = wrapAxiosWithPayment(axios.create(), client)
// Payment is handled transparently — same as fetch
const res = await api.get('https://blog.example.com/posts/article')
console.log(res.data)The axios adapter attaches a response interceptor that catches 402 responses,
calls client.buildPaymentSignature(), sets the PAYMENT-SIGNATURE header,
and retries the original request.
Custom transports (ky, got, undici, …)
For any other HTTP client, call buildPaymentSignature directly inside your
client's interceptor, hook, or middleware:
import { LedewirePaymentClient } from '@ledewire/x402-client'
import ky from 'ky'
const client = new LedewirePaymentClient({ key, secret })
const api = ky.create({
hooks: {
afterResponse: [
async (request, _options, response) => {
if (response.status !== 402) return response
const header = response.headers.get('payment-required')
if (!header) return response
const sig = await client.buildPaymentSignature(header, request.url)
return ky(request.url, {
headers: { 'PAYMENT-SIGNATURE': sig },
})
},
],
},
})The PaymentSigner interface is what both built-in adapters depend on —
you can also pass any object with a buildPaymentSignature method:
import type { PaymentSigner } from '@ledewire/x402-client'
const signer: PaymentSigner = {
async buildPaymentSignature(header, url) {
// custom implementation
},
}Getting a buyer API key
Create a key from a buyer account:
import { createClient } from '@ledewire/node'
const client = createClient()
await client.auth.loginWithEmail({ email, password })
const { key, secret } = await client.user.apiKeys.create({
name: 'my-agent',
spending_limit_cents: 5000, // $50 cap — optional
})
// Store secret immediately — it is never shown againOr create one at ledewire.com/settings/api-keys.
Error handling
import {
createLedewireFetch,
InsufficientFundsError,
NonceExpiredError,
UnsupportedSchemeError,
MalformedPaymentRequiredError,
} from '@ledewire/x402-client'
import { AuthError, LedewireError } from '@ledewire/core'
const fetch = createLedewireFetch({ key, secret })
try {
const res = await fetch('https://blog.example.com/posts/article')
} catch (err) {
if (err instanceof InsufficientFundsError) {
// Wallet has insufficient funds — top up at ledewire.com/wallet
} else if (err instanceof NonceExpiredError) {
// Payment nonce expired — retry the fetch from the beginning
} else if (err instanceof UnsupportedSchemeError) {
// The 402 was not a ledewire-wallet challenge — pass through or handle
} else if (err instanceof MalformedPaymentRequiredError) {
// Server sent a malformed PAYMENT-REQUIRED — likely a server misconfiguration
} else if (err instanceof LedewireError) {
console.error(err.statusCode, err.message)
}
}| Error | Cause |
| ------------------------------- | --------------------------------------------------------------- |
| InsufficientFundsError | Wallet balance too low (server returned 422) |
| NonceExpiredError | Payment nonce expired before retry — simply retry fetch() |
| UnsupportedSchemeError | 402 response uses a non-ledewire-wallet scheme |
| MalformedPaymentRequiredError | PAYMENT-REQUIRED header is invalid or missing required fields |
| AuthError | Buyer API key credentials are invalid (401 on payment) |
| LedewireError | All other Ledewire API errors — check .statusCode |
Token lifecycle
LedewireAuthManager caches the buyer JWT and re-authenticates automatically when the
token is within 60 seconds of expiry. For agents with lifetimes shorter than a single
token TTL (typically 30 minutes), authentication happens exactly once.
For long-running processes, the LedewireAuthManager instance inside the returned fetch
handles re-authentication transparently on every payment attempt.
Runtime compatibility
This package uses only web-standard APIs: fetch, atob, btoa, Response, Headers,
and URL. It has no Node.js-specific imports. It runs unchanged on:
- Node.js 18+
- Deno
- Cloudflare Workers
- Vercel Edge Runtime
- Any other runtime with standard
fetch
Related packages
@ledewire/node— full Ledewire API client for Node.js (buyer auth, wallet, purchases, merchant)@ledewire/browser— Ledewire SDK for browser applications
