An entitlement-gated feature
Sell a SKU once, gate access on read.entitled, and revoke it when the money comes back.
Source src/operations/entitlements.ts#L32grantEntitlementsrc/operations/spend.ts#L80spend
You are selling access — a world, a feature, an upload slot — not a stack of consumables. The recipe is to let the sale itself record ownership and to ask one reader everywhere access is decided.
A spend grants its sku to the buyer in the same
transaction that moves the money, so there is no separate fulfillment step to lose. From then on,
read.entitled answers the gate:
import { createEconomy, credits, spend, systemActor, topUp, userActor } from '@pwngh/economy-lab';
import type { SnippetReport } from './context.ts';
// Ownership is a record, not a balance: the spend that sells a SKU grants it in the same
// transaction, and `read.entitled` is the gate the rest of your service asks before it unlocks
// anything. No separate fulfillment step to lose.
export async function run(): Promise<SnippetReport> {
const economy = await createEconomy();
await economy.submit(
topUp({
idempotencyKey: 'idem_fund',
actor: systemActor('payments'),
userId: 'usr_g',
amount: credits(100),
source: 'card',
}),
);
const before = await economy.read.entitled('usr_g', 'wrld_cape');
await economy.submit(
spend({
idempotencyKey: 'idem_buy',
actor: userActor('usr_g'),
orderId: 'ord_g1',
buyerId: 'usr_g',
sku: 'wrld_cape',
price: credits(40),
recipients: [{ sellerId: 'usr_s', shareBps: 10_000 }],
}),
);
const after = await economy.read.entitled('usr_g', 'wrld_cape');
await economy.close();
return {
lines: [
`entitled('usr_g', 'wrld_cape') before the purchase: ${before}`,
'spend committed — the same transaction granted the SKU',
`entitled('usr_g', 'wrld_cape') after: ${after}`,
],
consolePath: '/market',
};
}The gate in your service is one guard clause:
async function equipCape(userId: string) {
if (!(await economy.read.entitled(userId, 'wrld_cape'))) {
return refuse('This feature needs the Cape.');
}
equip(userId, 'wrld_cape');
}
Two edges complete the recipe:
- Comped access is
grantEntitlement: the same ownership record without a sale, for support gestures and creator comps. - Money coming back takes the access with it. A
refundor a chargeback-driven revoke callsrevokeEntitlement, and the sameread.entitledgate closes everywhere at once.