Skip to main content

Why budget control matters

An AI agent calling paid APIs in a loop can rack up costs fast. Budget control lets you:
  • Cap total spend per session
  • Set per-domain limits (e.g., max 100 sats/session on api.weather.com)
  • Receive a callback before each payment
  • Get a full spending report at any time

Global budget

import { L402Client, BlinkWallet } from "l402-kit";

const client = new L402Client({
  wallet: new BlinkWallet(process.env.BLINK_API_KEY!, process.env.BLINK_WALLET_ID!),
  budgetSats: 500, // max 500 sats total this session
});
When a 402 response includes a priceSats field and it would exceed the remaining budget, the client throws BudgetExceededError before paying — no satoshis are spent.

Per-domain budget

const client = new L402Client({
  wallet,
  budgetSats: 2000,
  budgetPerDomain: {
    "api.weather.com": 100,
    "api.finance.com": 500,
  },
});
Per-domain limits are checked independently from the global limit — both must pass for the payment to proceed.

Callbacks

const client = new L402Client({
  wallet,
  budgetSats: 1000,
  onSpend: (sats, url) => {
    console.log(`✓ Paid ${sats} sats → ${url}`);
    // log to your telemetry, update a dashboard, etc.
  },
  onBudgetExceeded: (url, sats) => {
    console.warn(`✗ Blocked: ${sats} sats requested by ${url} — budget exhausted`);
    // alert, notify Slack, etc.
  },
});
onBudgetExceeded / on_budget_exceeded is called just before BudgetExceededError is thrown — useful for logging or alerts.

Spending report

const report = client.spendingReport();

if (report) {
  console.log(`Total spent: ${report.total} sats`);
  console.log(`Remaining:   ${report.remaining} sats`);
  console.log("By domain:", report.byDomain);
  // { "api.weather.com": 42, "api.finance.com": 105 }

  for (const tx of report.transactions) {
    console.log(`  ${tx.ts}  ${tx.sats} sats  ${tx.url}`);
  }
}
spendingReport() returns null / None when no budget is configured.

Handling BudgetExceededError

import { BudgetExceededError } from "l402-kit";

try {
  const res = await client.fetch("https://api.example.com/premium");
} catch (err) {
  if (err instanceof BudgetExceededError) {
    console.log(`Need ${err.required} sats, only ${err.remaining} remaining`);
    // gracefully degrade — return cached data, skip this step, etc.
  }
}

Concurrency notes

Do not share one L402Client instance across concurrent Promise.all calls when budget limits matter.BudgetTracker.check() and record() are separated by an await (the Lightning payment). Two concurrent client.fetch() calls to different endpoints can both pass the budget check before either records the spend — meaning the combined cost can temporarily exceed your budget cap by one payment.Safe pattern — sequential calls:
for (const url of urls) {
  const res = await client.fetch(url); // awaited one at a time
}
Risky pattern — parallel calls:
// Both may pass budget.check() before either calls budget.record()
const results = await Promise.all(urls.map(url => client.fetch(url)));
Mitigation for parallel workloads: set your budgetSats conservatively (e.g. 80% of your true limit) to absorb the over-spend from one concurrent payment. For strict enforcement, process calls sequentially.

Full options reference

OptionTypeScriptPythonDefaultDescription
Global budgetbudgetSatsbudget_satsunlimitedMax sats for the session
Per-domainbudgetPerDomainbudget_per_domain{}Map of domain → max sats
Spend hookonSpendon_spendCalled after each payment
Exceeded hookonBudgetExceededon_budget_exceededCalled before throwing