@ledewire/node
v0.18.0
Published
LedeWire SDK for Node.js — full API surface for building merchant stores
Maintainers
Readme
@ledewire/node
Node.js SDK for the LedeWire content marketplace — full API surface for building merchant stores, managing store content, and processing buyer flows on the server side.
Install
npm install @ledewire/nodeQuick Start
import { createClient } from '@ledewire/node'
// Store API key: key + secret grants read/write access to client.merchant.*
const client = createClient()
// loginWithApiKey resolves with the key's store_id: pass it to client.merchant.*
const { store_id } = await client.auth.loginWithApiKey({
key: process.env.LEDEWIRE_API_KEY,
secret: process.env.LEDEWIRE_API_SECRET,
})
const summary = await client.merchant.sales.summary(store_id)
const items = await client.merchant.content.list(store_id)
// Or pass the key to createClient: it logs in by itself before the first
// authenticated request (and again if a refresh fails), so no login call is needed.
const keyClient = createClient({
apiKey: process.env.LEDEWIRE_API_KEY,
apiSecret: process.env.LEDEWIRE_API_SECRET,
})
const sales = await keyClient.merchant.sales.list(storeId)
// Merchant email/password auth
const client = createClient()
await client.merchant.auth.loginWithEmail({ email, password })
const stores = await client.merchant.auth.listStores()Client Namespaces
| Namespace | Description |
| ------------------------------ | --------------------------------------------------------------------------------------------- |
| client.config | Platform-level public config (no auth required) |
| client.auth | Buyer signup, email/password login, Google OAuth, password reset, store invitation lookup |
| client.wallet | Buyer wallet balance (incl. held_cents/holds from bulk acquisitions) and payment sessions |
| client.purchases | Buyer purchase history, create purchases, verify ownership |
| client.content | Fetch content with buyer access info; search every Store and Publication |
| client.checkout | Checkout state — what action is required next |
| client.user.apiKeys | Manage buyer API keys for autonomous agents |
| client.user.spendCap | Buyer's daily spend cap — read and update the ceiling |
| client.user.mcpKeys | Manage buyer MCP API keys for the Ledewire MCP server |
| client.user.paymentKeys | Register, list and revoke the Ed25519 Payment keys that sign x402 payments |
| client.company | Company wallets: membership, invitations, members, Machine users, top-ups, spend reports |
| client.publications | Bulk-licensing catalog: publications and their works (public) |
| client.topics | Topic directory: the subjects publications cover, with how many carry each (public) |
| client.acquisitions | Bulk licensing: quote, authorize, corpus download, signed manifest |
| client.x402 | Public x402 Bazaar discovery, origin verification and the accessToken JWKS (no auth required) |
| client.merchant.auth | Merchant login (email / Google), store discovery, password reset, accept a store invitation |
| client.merchant.users | Merchant user management (invite, list, update, remove) |
| client.merchant.content | Content CRUD + search (merchant JWT or store API key) |
| client.merchant.buyers | Buyer statistics within a store |
| client.merchant.sales | Sales reporting and revenue statistics |
| client.merchant.config | Store configuration |
| client.merchant.domains | x402 domain verification for URL-based content gating |
| client.merchant.pricingRules | x402 URL pattern-based pricing rules |
Configuration
const client = createClient({
apiKey: process.env.LEDEWIRE_API_KEY,
apiSecret: process.env.LEDEWIRE_API_SECRET, // omit for read-only store API-key access
baseUrl: 'https://api-staging.ledewire.com', // optional, defaults to production
// Persist tokens across server restarts (optional)
storage: {
getTokens: async () => JSON.parse((await redis.get('lw:tokens')) ?? 'null'),
setTokens: async (t) => redis.set('lw:tokens', JSON.stringify(t)),
clearTokens: async () => redis.del('lw:tokens'),
},
// Side-effects only — storage.setTokens is already the persistence hook.
// Use onTokenRefreshed for audit logging or cache invalidation on refresh.
onTokenRefreshed: async (tokens) => {
await auditLog.record('token_refreshed', { expiresAt: tokens.expiresAt })
},
onAuthExpired: () => {
console.error('LedeWire session expired — re-authenticate')
},
})Token refresh is handled automatically — you never need to call a refresh method manually.
apiKey / apiSecret log in automatically: construction sends nothing, and the client logs in
with the key (POST /v1/auth/login/api-key) before its first authenticated request when no
token is stored. Concurrent first requests share one login. If a refresh fails later, the
client logs in with the key again and retries the request once. onAuthExpired fires only
when an automatic login fails, once per failed attempt. A refused key rejects the request with
an AuthError carrying the API's message. A client that already holds a token (from storage
or an explicit login) doesn't log in.
On a client created with apiKey, clearing the stored tokens doesn't drop the credentials: the next request logs in again. To stop for good, create a client without apiKey.
Don't mix another login (email, Google, merchant) on an apiKey client: if that session's refresh fails, the client falls back to the store key.
To get the key's store_id, call client.auth.loginWithApiKey() once yourself, as in the quick
start: it resolves with store_id, and the client, now holding a token, doesn't log in again.
Serverless / edge note: The default
MemoryTokenStorageresets on every cold start, which means tokens are lost between function invocations. Always provide a customstorageadapter (database, Redis, encrypted cookie) when deploying to serverless or edge runtimes.
Example: Merchant JWT Auth (no API key)
Use this flow when running a merchant backend that authenticates via email/password or Google. No API key is required. Token refresh is automatic — the SDK handles it transparently.
Email / password login
The one-step helper logs in and returns both the normalized tokens and the accessible stores list in a single HTTP call:
import { createClient, ForbiddenError } from '@ledewire/node'
const client = createClient({
// Required for serverless/edge — MemoryTokenStorage (default) resets on cold start.
storage: {
getTokens: async () => JSON.parse((await redis.get('lw:tokens')) ?? 'null'),
setTokens: async (t) => redis.set('lw:tokens', JSON.stringify(t)),
clearTokens: async () => redis.del('lw:tokens'),
},
onAuthExpired: () => redirect('/login'),
})
try {
const { tokens, stores } = await client.merchant.auth.loginWithEmailAndListStores({
email: '[email protected]',
password: process.env.MERCHANT_PASSWORD,
})
// tokens: StoredTokens — { accessToken, refreshToken, expiresAt: number (Unix ms) }
// stores: MerchantLoginStore[] — use .id, .name, .role
const storeId = stores[0].id
} catch (err) {
if (err instanceof ForbiddenError) {
// Valid credentials but account has no merchant store access (e.g. buyer account).
// err.message: "This account does not have merchant access. Use a merchant or owner account."
}
}Google OAuth login
Same flow with a Google ID token instead of email/password:
const { tokens, stores } = await client.merchant.auth.loginWithGoogleAndListStores({
id_token: googleIdToken, // from Google Identity Services callback
})
const storeId = stores[0].idPassword reset
Two-step flow — request a code, then submit it with the new password:
// Step 1 — send a 6-digit reset code to the merchant's email.
// Always returns 200 to prevent email enumeration.
await client.merchant.auth.requestPasswordReset({ email: '[email protected]' })
// Step 2 — submit the code and new password.
await client.merchant.auth.resetPassword({
email: '[email protected]',
reset_code: '246810',
password: 'new-secure-password',
})Separate login + store list (when you need full store detail)
Use this only when you need fields available on ManageableStore but not on MerchantLoginStore
(store_key, logo):
await client.merchant.auth.loginWithEmail({ email, password })
const stores = await client.merchant.auth.listStores() // ManageableStore[]
const storeId = stores[0].id // .id and .name match MerchantLoginStoreExample: Merchant Store Setup
const client = createClient()
await client.merchant.auth.loginWithEmail({
email: '[email protected]',
password: process.env.MERCHANT_PASSWORD,
})
const stores = await client.merchant.auth.listStores()
const storeId = stores[0].id
// Create a markdown article
await client.merchant.content.create(storeId, {
content_type: 'markdown',
title: 'Hello World',
content_body: btoa('# Hello World\nFull article body here.'),
price_cents: 500,
visibility: 'public',
})
// Create an external reference (e.g. a Vimeo video)
await client.merchant.content.create(storeId, {
content_type: 'external_ref',
title: 'Intro to Machine Learning',
content_uri: 'https://vimeo.com/987654321',
external_identifier: 'vimeo:987654321',
price_cents: 1500,
visibility: 'public',
})
// Inline HTML — content_body and content_uri are mutually exclusive for 'html'
await client.merchant.content.create(storeId, {
content_type: 'html',
title: 'Interactive Report',
content_body: '<h1>Q3 Results</h1><p>...</p>',
price_cents: 900,
visibility: 'public',
})
// Remote HTML, PDF, image, and video all require content_uri (never content_body)
await client.merchant.content.create(storeId, {
content_type: 'pdf',
title: 'Annual Report (PDF)',
content_uri: 'https://cdn.example.com/reports/annual-2026.pdf',
price_cents: 2000,
visibility: 'public',
})
const items = await client.merchant.content.list(storeId)
// items.data — ContentListItem[]
// items.pagination — PaginationMeta
// Search by title (partial match), URI, and/or metadata
const results = await client.merchant.content.search(storeId, { title: 'intro' })
const byUri = await client.merchant.content.search(storeId, { uri: 'vimeo.com' })
const combined = await client.merchant.content.search(storeId, {
title: 'tutorial',
metadata: { category: 'ml' },
})
// Fetch Google OAuth client ID before the user has signed in
const { google_client_id } = await client.config.getPublic()
// google.accounts.id.initialize({ client_id: google_client_id, callback })Full and view keys
A store API key logged in with its secret (client.auth.loginWithApiKey({ key, secret })) has
full permission; with the key alone it has view. Reads work with either. These need a full
key or an owner's merchant login, and a view key gets a 403 ForbiddenError:
merchant.content.create/update/delete(anis_authormember may write only their own items)merchant.users.list/invite/update/removemerchant.pricingRules.create/deactivatemerchant.domains.add/verify/remove
Sales rows and buyers
merchant.sales.get() names the buyer only by buyer_ref (bref_v1_…), an opaque, store-scoped
reference that matches merchant.buyers.list(). The buyer's user id and name are not disclosed.
fees.*_cents are null until the sale completes.
const sale = await client.merchant.sales.get(storeId, saleId)
const { data: buyers } = await client.merchant.buyers.list(storeId)
const buyer = buyers.find((b) => b.buyer_ref === sale.buyer_ref)Store invitations
merchant.users.invite() emails an invitation_token to someone without an account. Show the
invitation with the public auth.getInviteInfo(), then let the invitee join through
auth.signup() / auth.loginWithGoogle() with invitation_token, or, if they are already a
merchant, through merchant.auth.acceptInvitation():
import { LedewireError } from '@ledewire/node'
const invite = await client.auth.getInviteInfo(token) // no auth; 404 / 409 / 410 when unusable
console.log(`Join ${invite.store_name}`)
try {
await client.merchant.auth.loginWithEmail({ email, password })
const membership = await client.merchant.auth.acceptInvitation({ invitation_token: token })
console.log(`Joined ${membership.store_name} as ${membership.role}`)
} catch (err) {
if (err instanceof LedewireError && err.type === 'invitation_not_accepted') {
// err.details.reason: 'not_found' | 'wrong_email' | 'already_accepted' | 'already_member' | 'expired' | 'invalid'
}
}Example: Buyer Spend Cap & MCP API Keys
Every buyer starts with a default daily spend cap governing every wallet debit (MCP, REST, and the web payment gate). Read it or change it; it can't be removed:
const cap = await client.user.spendCap.get()
if (cap.remaining_cents !== null && cap.remaining_cents < 500) {
console.warn(`Only ${cap.remaining_cents}c left before the cap resets at ${cap.resets_at}`)
}
await client.user.spendCap.update({ daily_spend_limit_cents: 2000 }) // raise to $20/dayMCP API keys authenticate agent requests to the Ledewire MCP server. The
secret is shown once at creation — store it immediately:
const { key, secret } = await client.user.mcpKeys.create({
label: 'my-rag-agent',
can_search: true,
can_purchase: true,
})
await secretsManager.put('LEDEWIRE_MCP_CREDENTIAL', `${key}:${secret}`)
const keys = await client.user.mcpKeys.list() // secrets never included
await client.user.mcpKeys.revoke(keys[0].id) // to change scopes: revoke + recreatePayment keys are the public halves of the Ed25519 keys that sign x402 payments.
A Payment key is never a login credential, and LedeWire never holds the private
half, so there is no secret to store from the response — keep the private key
yourself. Prove you hold it by signing ledewire-payment-key: + public_key:
import { generateKeyPairSync, sign } from 'node:crypto'
const { publicKey, privateKey } = generateKeyPairSync('ed25519')
// Unpadded base64url of the 32 raw public-key bytes (the tail of the SPKI DER)
const public_key = publicKey
.export({ type: 'spki', format: 'der' })
.subarray(-32)
.toString('base64url')
const message = Buffer.from(`ledewire-payment-key:${public_key}`, 'ascii')
const proof = sign(null, message, privateKey).toString('base64url')
const key = await client.user.paymentKeys.create({ name: 'research-agent', public_key, proof })
const keys = await client.user.paymentKeys.list() // a plain array, newest first
await client.user.paymentKeys.revoke(key.id)A Buyer holds at most 10 active Payment keys, so rotate by registering the
replacement before revoking the old key. Only buyer tokens may call these
routes (others get 403 Invalid Role); an invalid proof, a malformed key or a
duplicate key is refused with 422. Signing payments with the key is
@ledewire/x402-client's job.
Example: Company Wallets
A Company is a shared wallet that pays for its members' purchases. Each member
spends under their own daily Spend cap, set by a Company admin, and never
sees the Company balance: for a member, wallet.balance() returns
balance_cents: null / spendable_cents: null, content.getWithAccess()
returns wallet_balance_cents: null, and remaining_cents / company_name
say what they may still spend today and whose wallet pays. Handle null
before doing arithmetic on a balance.
// Any buyer: am I in a Company?
const wallet = await client.wallet.balance()
if (wallet.company_name !== null) {
console.log(`${wallet.company_name} pays; ${wallet.remaining_cents}c left today`)
}
const membership = await client.company.membership.get() // NotFoundError if none
// Admin: invite someone. Nobody joins until they accept with the emailed token —
// an existing buyer via invitations.accept(), a new one via auth.signup() or
// auth.loginWithGoogle(). A token that can't be accepted refuses the signup (422,
// err.type === 'invitation_not_accepted', err.details.reason says why).
const invitation = await client.company.invitations.create({ email: '[email protected]' }) // role defaults to 'member'
// await client.company.invitations.revoke(invitation.id) // to withdraw it while still pending
await client.company.invitations.accept({ token }) // invitee, existing account
await client.auth.signup({ email, password, name, company_invitation_token: token }) // invitee, new account
// Admin: set a member's daily Spend cap (by membership id; never null).
const { data: members } = await client.company.members.list()
await client.company.members.update(members[0].id, { daily_spend_limit_cents: 5000 })
// Admin: a Machine user is an agent identity with no login, spending the Company's money.
const bot = await client.company.machineUsers.create({ name: 'research-agent' })
const { key, secret } = await client.company.machineUsers.buyerKeys.create(bot.id, {
name: 'production',
})
const agent = createAgentClient({ key, secret }) // logs in as the Machine user on its first call
await client.company.machineUsers.mcpKeys.create(bot.id, {
label: 'research-agent',
scopes: ['mcp:search', 'mcp:purchase'],
})
// Its x402 Payment key: the agent holds the private half; the admin registers
// the public half with a proof built as for user.paymentKeys.create().
await client.company.machineUsers.paymentKeys.create(bot.id, {
name: 'production',
public_key,
proof,
})
const { data: paymentKeys } = await client.company.machineUsers.paymentKeys.list(bot.id) // { data } envelope
await client.company.machineUsers.update(bot.id, { name: 'nightly-agent' }) // keys keep working
await client.company.machineUsers.deactivate(bot.id) // permanent; revokes every key
// Admin: read the Company wallet — the only place its balance appears.
const { balance_cents, held_cents, pending_top_up_cents } = await client.company.wallet.get()
// Admin: fund the Company wallet (card or ACH) and track unsettled top-ups.
// wallet.getPaymentStatus() covers personal top-ups only — it does not find a Company session.
await client.company.wallet.createPaymentSession({ amount_cents: 50000, currency: 'usd' })
const { data: pending } = await client.company.wallet.listPendingTopUps()
// Admin: what the Company paid for, and what each member spent.
const { data: purchases } = await client.company.purchases.list({ kind: 'bulk_acquisition' })
const { data: spend } = await client.company.spend.list({ from: '2026-09-01', to: '2026-09-30' })Everything except membership and invitations.accept() is Company-admin only
and throws ForbiddenError for a plain member. A member cannot change their own
cap with user.spendCap.update() (403) — an admin sets it via
company.members.update(). A Machine user cannot manage its own keys through
user.apiKeys / user.mcpKeys / user.paymentKeys (403).
Example: Bulk Licensing (Acquisitions)
Buy a publication's entire catalog (or a date-bounded slice of it) in one transaction instead of purchasing works one at a time:
// 1. Browse the catalog and pull a page of work URLs. coverage_horizon says how
// far back LedeWire has swept a publication (null = not established yet).
// Narrow the catalog by Topic. Topic slugs come from `client.topics.list()`.
// To load one publication directly, use `client.publications.get(id)`.
const { data: publications } = await client.publications.list({
bulk_licensable: true,
topics: ['sports'],
})
const publication = publications[0]
const { works } = await client.publications.listWorks(publication.id, {
from: '2026-01-01',
to: '2026-01-31',
page_kind: 'article', // optional: a filtered page may be empty before next_cursor is null
})
// 2. Submit the Selection. Quoting is asynchronous: poll for as long as the
// response carries poll_after_seconds (2–60s, sized from the Selection).
const wait = (s: number) => new Promise((r) => setTimeout(r, s * 1000))
let acquisition = await client.acquisitions.create({ urls: works.map((w) => w.url) })
while (acquisition.poll_after_seconds !== undefined) {
await wait(acquisition.poll_after_seconds)
acquisition = await client.acquisitions.get(acquisition.id)
}
// Changed your mind? A quote can be withdrawn — no money moves:
// await client.acquisitions.cancel(acquisition.id)
// 3. Acknowledge whatever cannot be sold — required before authorizing.
acquisition = await client.acquisitions.acknowledgeExclusions(acquisition.id)
// 4. Place the hold and start the run.
try {
acquisition = await client.acquisitions.authorize(acquisition.id)
} catch (err) {
if (err instanceof SpendCapReachedError) {
// Funding the wallet will not clear this — it's a daily policy limit.
console.error(`Spend cap reached; resets at ${err.resetsAt}.`)
return
}
throw err
}
// 5. Poll until the run settles, then page through only the failures.
while (acquisition.poll_after_seconds !== undefined) {
await wait(acquisition.poll_after_seconds)
acquisition = await client.acquisitions.get(acquisition.id)
}
const { data: failed } = await client.acquisitions.listWorks(acquisition.id, {
delivery_state: 'undelivered', // also: line_state, exclusion_reason
})
// 6. Stream the corpus archive to a file.
import { createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
import { Readable } from 'node:stream'
const result = await client.acquisitions.downloadCorpus(acquisition.id)
if (result.ready) {
await pipeline(Readable.fromWeb(result.body), createWriteStream('corpus.tar.gz'))
}
// 7. Verify the signed manifest against the append-only signing-key log —
// never trust the `jwk` embedded in the manifest's own JWS header.
const manifest = await client.acquisitions.getManifest(acquisition.id)
const keyHistory = await client.acquisitions.signingKeyHistory()Example: Buyer Content Search
Search every Store and every brokered Publication from a Buyer's server. Page with
next_cursor until it is null. The request is a ContentSearchRequest (a Store's search
of its own content, client.merchant.content.search(), takes a MerchantContentSearchRequest):
import type { ContentSearchRequest } from '@ledewire/node'
let cursor: string | null | undefined
do {
const request: ContentSearchRequest = { query: 'quantum computing', cursor, limit: 20 }
const page = await client.content.search(request)
for (const result of page.data) {
console.log(result.title, result.publication?.name ?? '(Store work)')
}
// partial_results: true means part of this page could not be searched. next_cursor
// still points at what was missed, so sending it again asks for those results.
cursor = page.next_cursor
} while (cursor)Searches with a query are rate-limited to 15 a minute per Buyer. A 429 throws a
RateLimitError (a LedewireError with statusCode 429).
A cursor lasts 24 hours and fits only the search it came from. One that is expired, invalid,
or sent with different filters throws InvalidCursorError (statusCode 422,
type: 'invalid_cursor') and nothing is searched. Drop the cursor and restart from the first
page; the same cursor never succeeds on retry:
import { InvalidCursorError } from '@ledewire/node'
try {
page = await client.content.search({ query: 'quantum computing', cursor })
} catch (err) {
if (!(err instanceof InvalidCursorError)) throw err
page = await client.content.search({ query: 'quantum computing' }) // restart
}Narrow to one Publication with publication (its id, or its exact name in any case).
To search SEC EDGAR filings, pass form_type, ticker or cik: only filings come back,
and query can be left out to list them newest first. Each result's filing describes
the filing (filer_name, ticker, cik, form_type, filing_date,
period_of_report, accession_number), and is null for every other work.
Filings are shown to every signed-in Buyer, up to that Buyer's limit on SEC requests;
over it, the search returns what it could with partial_results: true.
// Apple's annual reports, amendments (10-K/A) included.
const { data } = await client.content.search({ ticker: 'AAPL', form_type: '10-K' })
for (const result of data) {
console.log(result.filing?.form_type, result.filing?.filing_date, result.title)
}Example: Purchase Licences
Every purchase carries license (a PurchaseLicense, or null when none was recorded —
every Store work, and older brokered purchases; null never means "no rights"), plus
content.publication (the brokered work's Publication, or null) and content.filing
(the SEC filing, or null). license.type is the source's own licence type, such as
ON_DEMAND_LICENSE, ON_DEMAND_FULL_USE_LICENSE or public-domain for an SEC filing.
When license.document_url is set, purchases.licenseDocument() downloads the document.
It is not JSON: you get { body: ArrayBuffer, contentType }, usually application/pdf,
or text/markdown for an SEC filing's terms.
import { writeFile } from 'node:fs/promises'
const purchase = await client.purchases.get(purchaseId)
console.log(purchase.license?.type, purchase.content.publication?.name)
if (purchase.license?.document_url) {
const { body, contentType } = await client.purchases.licenseDocument(purchase.id)
const ext = contentType.startsWith('application/pdf')
? 'pdf'
: contentType.startsWith('text/markdown')
? 'md'
: 'bin'
const file = `licence.${ext}`
await writeFile(file, Buffer.from(body))
}Example: x402 Bazaar Discovery
Public, unauthenticated browsing of resources gated by the x402
ledewire-wallet scheme:
const { total, resources } = await client.x402.discoverResources({ limit: 20 })
for (const resource of resources) {
console.log(resource.metadata.title ?? '(untitled)', resource.metadata.teaser)
}Pay for a discovered resource via the x402 flow itself — see
@ledewire/x402-client — not through this namespace, which
never carries delivered content.
Confirm a page URL is a registered Ledewire resource before showing payment UI,
and fetch the JWKS that verifies a settlement accessToken offline:
const origin = await client.x402.verifyOrigin({ url: 'https://theirsite.com/articles/slug' })
if (origin.verified) console.log(origin.title, origin.amount)
const { keys } = await client.x402.getJwks() // cache for 24h; match the token's kidverifyOrigin answers { verified: false } for an unknown URL rather than throwing,
and throws RateLimitError (with retryAfter) past 60 requests a minute per IP.
getJwks throws a LedewireError with statusCode 503 and message
'JWKS unavailable' when the server has no signing key configured.
Error Handling
All SDK errors extend LedewireError — use instanceof checks on named subclasses:
import { createClient, ForbiddenError, AuthError } from '@ledewire/node'
try {
await client.merchant.auth.loginWithGoogle({ id_token })
} catch (err) {
if (err instanceof ForbiddenError) {
// 403 — credentials are VALID but the account has no merchant store access.
// This is the expected error when a personal Google account previously
// registered as a buyer is used on the merchant login endpoint.
// err.message → "This account does not have merchant access. Use a merchant or owner account."
// Fix: use a dedicated merchant/owner account, or have a store owner add your account.
console.error('Wrong account role:', err.message)
} else if (err instanceof AuthError) {
// 401 — wrong email/password, API key or Google token. err.message is the
// API's message (e.g. "Invalid email or password"). A rejected login never
// refreshes, retries, or clears a session the client already holds.
console.error('Authentication failed:', err.message)
}
}| Subclass | Status | When thrown |
| ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| AuthError | 401 | Invalid credentials on any login, expired token, failed token refresh |
| ForbiddenError | 403 | Valid credentials, wrong account role (e.g. buyer on merchant endpoint) |
| NotFoundError | 404 | Resource not found |
| PurchaseError | 409/422 | Purchase validation failure (price mismatch, duplicate, etc.) |
| SpendCapReachedError | 402 | Buyer's daily spend cap reached — purchases.create(), the x402 content gate, and acquisitions.authorize() |
| RateLimitError | 429 | Too many requests, from any endpoint (content.search: 15/min per buyer); retryAfter is seconds to wait |
| InvalidCursorError | 422 | content.search refused its cursor (type: 'invalid_cursor'): drop the cursor and restart the search |
| LedewireError | any | Catch-all base class for all other API errors |
Every LedewireError also carries type (the API error body's machine-readable
error.type, e.g. 'daily_spend_cap_reached', 'insufficient_funds') and
details (any extra top-level fields on the error body). The bulk acquisition
steps refuse with exclusions_unacknowledged, quote_not_ready,
quote_expired, quote_in_progress, invalid_acquisition_state (with
status / expected_status in details), nothing_to_hold, and
run_not_started (503 — nothing was held; safe to retry). instanceof checks
work even across the separately bundled copies of @ledewire/core inside
@ledewire/node, @ledewire/browser, and @ledewire/x402-client — an error
thrown by the x402-client is still recognized by SpendCapReachedError
imported from @ledewire/node.
SpendCapReachedError — funding the wallet does not clear it:
import { SpendCapReachedError } from '@ledewire/node'
try {
await client.purchases.create({ content_id, price_cents })
} catch (err) {
if (err instanceof SpendCapReachedError) {
// A daily policy limit, not a balance problem — do NOT prompt the buyer
// to fund their wallet. It clears when the window rolls over at
// err.resetsAt, or when the cap is raised via client.user.spendCap.update().
console.error(
`Spend cap reached: spent ${err.spentCents} of ${err.capCents} cents. Resets at ${err.resetsAt}.`,
)
}
}Documentation
License
MIT
