Scheduled top-ups
A recurring allowance from any timer, made safe by minting the idempotency key from the period.
Source src/operations/topUp.ts#L30topUpsrc/contract.ts#L76idempotencyKey
A monthly allowance, a daily login grant, a weekly stipend — every recurring credit is the same recipe: any timer you already have, plus an idempotency key minted from the period instead of from the moment.
The timer is deliberately not the interesting part. A cron job, a queue with a delay, or the same
Scheduler seam the background worker runs on all work,
because the safety lives in the key: fire twice in one period and the second submit is a
duplicate, not a second grant.
import {
createEconomy,
credits,
encodeAmount,
spendable,
systemActor,
topUp,
} from '@pwngh/economy-lab';
import type { SnippetReport } from './context.ts';
// A scheduled top-up is any timer plus an idempotency key minted from the period. The key is
// what makes the schedule safe: a cron that fires twice, a retried job, a redeployed worker —
// same period, same key, at most one grant.
export async function run(): Promise<SnippetReport> {
const economy = await createEconomy();
const monthly = (period: string) =>
economy.submit(
topUp({
idempotencyKey: `allowance_usr_kid_${period}`, // the period IS the key
actor: systemActor('allowance'),
userId: 'usr_kid',
amount: credits(50),
source: 'allowance',
}),
);
const july = await monthly('2026-07');
const julyAgain = await monthly('2026-07'); // the timer double-fired
const august = await monthly('2026-08');
const balance = await economy.read.balance(spendable('usr_kid'));
await economy.close();
return {
lines: [
`2026-07 fired: ${july.status}`,
`2026-07 re-fired: ${julyAgain.status} — the key absorbed the retry`,
`2026-08 fired: ${august.status}`,
`two months, one retry, balance: ${encodeAmount(balance)}`,
],
consolePath: '/market',
};
}The key discipline is the whole trick, and it is the same one every
idempotent retry uses: derive the key from the action’s own
identity (allowance_usr_kid_2026-07), never from Date.now() or a random id at fire time. A
timer that misfires, a job that retries after a crash, and a deploy that replays a backlog all
collapse into the one grant the period was owed.