
Keep a balance for prepaid work
Credit pools let a customer buy capacity before using it. Each pool tracks available credits, total purchases and total consumption for its billing account.
Wildo supplies guarded balance changes and a checkout settlement path for configured credit packs. Your backend decides when an activity consumes credits and how many it costs.
Example — Spend from a prepaid document balance
A customer purchases a pack, then a backend document action consumes credits before carrying out the paid work. Two concurrent spends cannot both use the same last credits.
For engineers
A CREDIT_PACK product needs purchaseConfig.creditPoolKey and its creditsPerUnit; the product type alone is insufficient. Register the product and complete checkout through the verified provider event path. The creditPools.enabled configuration flag is a declared product setting, not the runtime switch that activates these operations.
This complete illustrative product sells a pack of 100 document credits. Its mapping is explicit; the similarly named Wonder Todos sample credit pack does not currently supply this purchase mapping:
import {
ProductType, ResourcePrimaryScope, type ProductDefinition,
} from '@wildo-ai/saas-models';
import { PricingModel } from '@wildo-ai/external-connectors-models';
import { AvailableCurrency } from '@wildo-ai/zod-decorators';
export const documentCreditPack: ProductDefinition = {
key: 'document-credit-pack',
type: ProductType.CREDIT_PACK,
targetScopes: [ResourcePrimaryScope.ORGANIZATIONS],
purchaseConfig: {
creditPoolKey: 'documents',
creditsPerUnit: 100,
maxQuantityPerPurchase: 10,
},
prices: [{
model: PricingModel.FLAT,
unitPrice: { amount: 2500n, decimals: 2, currency: AvailableCurrency.USD },
intervalCount: 1,
isDefault: true,
}],
};
Register it in the shared module’s product catalogue and complete the configured checkout path. In this example, quantity two costs two packs and maps to 200 credits in the account’s documents pool. The verified payment-event handler applies the top-up; the checkout-return page does not award credits.
The settlement handler reads that mapping and computes the top-up:
// Read product to check if it's a CREDIT_PACK
const products = await this.servicesRegistry.list<Product>(
CoreResourceType.PRODUCTS, execCtx, { key: productKey },
) as Product[];
const product = products.length > 0 ? products[0] : null;
if (product?.type === ProductType.CREDIT_PACK && product.purchaseConfig?.creditPoolKey) {
const credits = (product.purchaseConfig.creditsPerUnit ?? 1) * quantity;
const poolKey = product.purchaseConfig.creditPoolKey;
// Find the credit pool, auto-create if first purchase
let pools = await this.servicesRegistry.list<CreditPool>(
variants.creditPool, execCtx,
{ [variants.billingAccountFkField]: billingAccountId, poolKey },
) as CreditPool[];
This selected engine fragment precedes pool creation if needed and the internal TOP_UP call. The surrounding handler uses a durable settlement claim before effects; it does not grant credits from the browser’s checkout-return URL. Claim acquisition, credit top-up and the applied marker are separate writes: the guarded debit protects the balance, but this example does not establish crash-atomic payment settlement.
Consume credits from trusted service code
CreditPool_Operations.CONSUME and TOP_UP are INTERNAL_CALL operations. Resolve the pool for the correct organisation/user billing account and invoke consumption from the backend workflow that owns the paid action. The browser cannot award itself a balance.
This complete illustrative backend helper uses the public service registry. Its caller must first authorize the paid action, resolve its organization billing account server-side, and supply an execution context appropriate for these internal billing operations. The account identifier and credit price are trusted inputs from that workflow, not values copied from a browser request:
import {
CoreResourceType, CreditPool_Operations, type CreditPool,
} from '@wildo-ai/saas-models';
import type {
ExecutionContext, ServicesRegistryHandlerBackendService,
} from '@wildo-ai/saas-backend-lib';
export async function consumeDocumentCredits(
services: ServicesRegistryHandlerBackendService,
context: ExecutionContext<any>,
organizationBillingAccountId: string,
credits: number,
): Promise<CreditPool | null> {
const pools = await services.list<CreditPool>(
CoreResourceType.ORGANIZATION_CREDIT_POOLS,
context,
{ organizationBillingAccountId, poolKey: 'documents' },
);
const pool = pools[0];
if (!pool) return null;
return services.executeCustomOperation<CreditPool>(
CoreResourceType.ORGANIZATION_CREDIT_POOLS,
CreditPool_Operations.CONSUME,
context,
pool._id,
{ credits },
);
}
The helper selects by billing account and pool key before using the resulting ID. A user-owned purchase uses the separate user credit-pool resource and userBillingAccountId; substituting only the ID would not establish that ownership.
Await consumption before starting the paid work. null means no matching pool exists; the caller can offer purchase and must not proceed. An insufficient balance rejects with BILLING_INSUFFICIENT_CREDITS; let the application’s normal error handling explain that refusal. Other storage or execution failures also stop this helper from returning a successful spend. The declared DTO requires at least one credit.
For a pool with ten available credits, consuming three returns seven available and raises totalConsumed by three. Attempting to consume eight next is refused and does not debit the seven. This illustrates the balance contract, not a new executed payment journey.
The consume implementation performs this guarded change:
const { credits } = input;
const result = await utils.repositoriesRegistry.atomicGuardedMutation(
variantResourceType,
{ _id, available: { $gte: credits } },
{ increment: { available: -credits, totalConsumed: credits } },
executionContext,
);
The adapter-neutral mutation requires available >= credits, decrements availability and increments consumption together. Insufficient balance returns a business-rule refusal. Addition increments both availability and purchased totals, preserving concurrent top-ups.
Decide what happens around the paid work
Balance safety does not decide your business workflow. Specify when to consume, how to handle work that fails after consumption and which operation can compensate it. A metered-usage declaration does not automatically consume prepaid credits: metering records a quantity for later billing; a credit pool spends existing capacity.
For verification, test a paid settlement, a repeated delivery and competing spends near zero. Keep checkout activation and product mapping in the setup: the sample catalogue’s credit-pack name alone does not prove those prerequisites are authored.