Skip to content

Guides

Recipe: API credits

Sell prepaid credits for an API or an AI feature and spend one per request. A short balance answers 402 with a top-up link.

View as Markdown

Buyers prepay a pack of credits; every request spends some. Perly Pay is the ledger: spending is one atomic call that refuses an overdraft, and every buyer has their own top-up page. Nothing is ever debited on its own — when credits run out, the buyer tops up. The whole credits feature is in Sell credits; this is the recipe around it.

With an assistant: the sell-credits skill of the Perly Pay plugin for Claude Code does this recipe in your project — the plugin and its example, examples/api-credits (MIT), are on their way. Until then, any assistant can read the same skill from /.well-known/skills/perly-pay-sell-credits/SKILL.md.

How it works

  1. 1

    A credit pack

    A product and a price with model: "credit_pack", unit_amount in cents and credits. Made once per mode.

  2. 2

    Your users are customers

    Made once with external_id (your user id); from then on credits.* take that id.

  3. 3

    Top up

    credits.topUpUrl(userId) is the buyer's own page: they pick a pack and pay. Each paid pack adds credits.

  4. 4

    Spend per request

    credits.consume(userId, cost, { idempotencyKey }) first, then the work. A retry with the same key returns the first spend.

  5. 5

    Short?

    insufficient_credits (409) spends nothing: answer 402 with the top-up page, not a 500.

Spend one request's credits
try {
  await pay.credits.consume(userId, cost, { product: PRODUCT, action: "api", idempotencyKey: `req-${userId}-${requestId}` });
} catch (e) {
  if (e.code !== "insufficient_credits") throw e;
  return reply(402, { top_up: await pay.credits.topUpUrl(userId, { product: PRODUCT }) });
}
// now do the work

Run the example

  1. 1

    Install

    npm install in examples/api-credits (Node 20.12+).

  2. 2

    Key

    Copy .env.example to .env and put a test key (sk_test_…) in PERLY_PAY_SECRET_KEY.

  3. 3

    Set up

    npm run setup makes the product and price with your key and prints their ids for .env.

  4. 4

    Run

    npm start (or npm run dev), as its README says.

  5. 5

    Pay without a wallet

    Call POST /v1/shout (402), open the page POST /top-up gives, pick the pack, press Simulate payment on the invoice, call again: 200 and X-Credits-Remaining: 499.

Pitfalls

  • Spending without an idempotency key: a retried request is charged twice.
  • Checking the balance, then spending, as two steps. consume alone refuses an overdraft; the balance is for display.
  • Doing the work before spending. Spend first.
  • Test ids with a live key. Products, prices and webhook endpoints belong to one mode: run the setup again with sk_live_… when you go live.