Checked on 6 Nov 2026 against the paas.build reference files
How to sell AI credits with payment links (2026)
Sell each credit pack through a payment link, then add the purchased credits only after your server verifies payment. Keep a prepaid balance, deduct credits for each AI call, and reject calls that exceed it. Store each payment once so retries cannot grant the same pack twice.
What does a credit buy?
Define one unit before asking Cursor, Claude Code, Lovable, Bolt, or Base44 to build checkout. For example, one credit buys one standard generation, and a pack contains 100 credits. These are example product rules. If different models consume different amounts, display the cost before the user starts a job.
Keep the balance on your server. Record grants, usage deductions, and refund adjustments in a ledger. Reserve or deduct credits atomically before calling the model. Never let the available balance fall below zero. Give each job a unique ID so retrying a request does not charge twice. Release a reservation once if the job fails under your stated policy.
How do I create a payment link for a pack?
The paas.build agent guide documents POST https://paas.build/api/checkout with vendorId, amount, currency, description, and env. It returns shortLink and sessionToken. Send Authorization: Bearer <access token> from your backend. Keep tokens scoped to their vendor and environment. Your agent can also use create_checkout; check the docs.
Create one checkout session for the selected pack purchase. Read its amount and credit count from your server catalog. The browser sends a pack identifier, not an amount you accept as authoritative. Save a pending order for the authenticated user before redirecting them to shortLink.
export async function createPackCheckout({ token, vendorId, pack }) {
const response = await fetch("https://paas.build/api/checkout", {
method: "POST",
headers: {
"Authorization": "Bearer " + token,
"Content-Type": "application/json"
},
body: JSON.stringify({
env: "sandbox", vendorId,
amount: pack.amount, currency: pack.currency,
description: pack.description
})
});
if (!response.ok) throw new Error("Checkout creation failed");
const { shortLink, sessionToken } = await response.json();
return { shortLink, sessionToken };
}
This ESM function uses sandbox checkout. Supply pack from trusted configuration. Save the returned session association against the pending order before exposing the link. If creation times out, reconcile the pending attempt before retrying; the reference does not document checkout-creation idempotency.
Where do user ID and pack size belong?
Carry both as purchase metadata: an internal user ID, pack identifier, and purchased credit count. Save an immutable snapshot with the expected amount and currency. Avoid putting personal details into a public link. A catalog change tomorrow must not change what yesterday's buyer receives.
The supplied paas.build reference does not specify a checkout metadata object. Keep this metadata in your own order record. The MCP create_checkout tool lists reference as an order or invoice reference, but its return path through payment verification is unspecified. Before sending metadata or mapping a paid record to your order, check the docs. Do not assume sessionToken is a payment ID.
Where do I credit the account?
Credit it on the server after a verified webhook or authenticated server-side payment lookup. Never credit from the client redirect, a query-string flag, or a browser callback. Show a pending confirmation screen while your server reconciles the purchase. A successful checkout-creation response only gives you a checkout session.
The same pattern works with any processor's payment links: verify payment, resolve the order, and grant once. For example, Stripe documents payment-link fulfillment through webhooks. That does not establish a paas.build webhook contract. Its supplied files omit payment lookup, signature verification, and final paid-status fields; check the docs before wiring those adapters.
Save this as credits.mjs. It exports an Express-compatible handler. Pass a persistent SQLite connection, authenticated-request verification, provider payment lookup, and your order lookup. These injected functions must be implemented; there is no default that accepts unverified payments. Use a rate-limited internal route for your reconciliation worker. Return only an order associated with the verified payment.
export function makeCreditHandler({
db, verifyRequest, lookupPayment, findOrder
}) {
db.exec(`CREATE TABLE IF NOT EXISTS credit_grants (
env TEXT NOT NULL, vendor_id TEXT NOT NULL,
payment_id TEXT NOT NULL, order_id TEXT NOT NULL UNIQUE,
user_id TEXT NOT NULL, credits INTEGER NOT NULL CHECK(credits > 0),
PRIMARY KEY (env, vendor_id, payment_id)
)`);
const grant = db.prepare(`INSERT INTO credit_grants
(env, vendor_id, payment_id, order_id, user_id, credits)
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING`);
return async function credit(req, res, next) {
try {
const paymentId = await verifyRequest(req);
if (typeof paymentId !== "string" || !paymentId) {
return res.sendStatus(401);
}
const p = await lookupPayment(paymentId);
if (!p || p.id !== paymentId || p.paid !== true) {
return res.sendStatus(409);
}
const order = await findOrder(p.orderId);
if (!order || order.id !== p.orderId ||
order.refundPending || order.refunded ||
p.env !== order.env || p.vendorId !== order.vendorId ||
p.currency !== order.currency ||
!Number.isSafeInteger(p.amountMinor) ||
p.amountMinor !== order.amountMinor ||
!Number.isSafeInteger(order.credits) || order.credits <= 0) {
return res.sendStatus(409);
}
const result = grant.run(p.env, p.vendorId, p.id,
order.id, order.userId, order.credits);
return res.json({ credited: result.changes === 1 });
} catch (error) {
next(error);
}
};
}
All fields on p and order are application-defined, not paas.build response fields. verifyRequest authenticates the caller and returns a payment ID. lookupPayment must retrieve authoritative payment state and its confirmed order association. Normalize amounts to integer minor units before comparison. findOrder loads your saved purchase metadata.
The database insert is the credit grant. Your balance reads this ledger alongside usage and refund entries. The unique payment key prevents duplicate grants; the unique order key prevents two payments granting one order. Reconcile an accidental second payment separately. Mount the returned handler in Express with your error middleware. For the database connection, see Node's SQLite reference.
What if the user refunds?
Choose and publish a policy before selling. One policy pattern is to refund eligible unused credits and remove those credits from the original pack. Track consumption per pack so you can calculate the unused portion. Treat this as your proposed product policy, subject to applicable obligations, rather than a paas.build refund rule.
Reserve the refundable credits while the refund is pending, so the user cannot spend them simultaneously. On confirmed success, record the refund adjustment once. On failure, release the reservation. If the full payment is reversed after usage, record the shortfall separately and review the account. Do not make spendable credits negative. For refund execution and status verification, check the docs.
How do I stop free-credit abuse?
Rate-limit checkout creation, confirmation requests, and the credit endpoint. Require authentication and check order ownership. Keep provider credentials off the client. Match vendor, environment, currency, and amount before granting anything. A valid sandbox payment must never fund a production account.
Have your coding agent test duplicate confirmations, simultaneous requests, mismatched amounts, and a lost redirect. Test a paid lookup arriving during a refund. Serialize grant and refund changes for each order in production. Alert on paid orders without grants, then replay reconciliation safely. These checks belong in your app even when checkout itself is hosted.
What else should I know?
Can I sell credits without a subscription?
Yes. Use a one-off checkout for each pack purchase. Keep the credit balance in your app and grant it after payment verification. You can add a recurring allowance later without changing the meaning of purchased credits.
Should every buyer share the same pack link?
Prefer a checkout session tied to each authenticated purchase. A shared link needs a documented way to associate each completed payment with its buyer. A user ID appended by the browser is not sufficient proof of ownership.
Does the return to my app prove payment?
No. Let the return page read your server-side order status. Grant credits only through verified reconciliation, and keep the same payment uniqueness check when a worker retries.
For current charges, see pricing.
Build the payment flow
Review paas.build subscriptions and the agent docs with your coding agent.
See also: Dodo Payments alternatives (2026), Paddle alternatives (2026) and the alternatives hub.