Downcity City DocsGuides
Hooks and Billing Logic
Connect quotas, usage, logs, and billing into the Action lifecycle.
Downcity hooks have three layers: Action -> Service -> Global. Execution order runs from outer to inner layers:
global.before
-> service.before
-> action.before
-> action.run() <- core logic
-> action.after <- billing
-> service.after
-> global.afterAction-level hooks
Each Action has its own hooks, which makes them a good fit for studio-specific logic:
const zh2en = svc.action("zh2en", async (ctx) => {
return await translate(ctx.input.text, "en");
});
// Check balance before the call
zh2en.before(async (ctx) => {
const bal = await balance.get(ctx.user!.user_id);
if (bal <= 0) throw new Error("Insufficient balance");
});
// Deduct balance after the call (ctx.output is available)
zh2en.after(async (ctx) => {
await deductBalance(ctx.user!.user_id, 10);
await logUsage(ctx);
});Service-level hooks
All Actions under the same Service share them:
svc.hook.before(async (ctx) => {
console.log(`${ctx.user?.id} -> ${ctx.service?.id}.${ctx.action?.id}`);
});
svc.hook.after(async (ctx) => {
// aggregate metrics, unified logs
});Comparison
| Action hook | Service hook | |
|---|---|---|
| Granularity | Single action | All actions in the service |
| Use cases | Balance checks, billing, per-action logs | Shared validation, aggregate metrics |
| Registration | action.before(fn) | svc.hook.before(fn) |
Key fields inside ctx
interface Context {
input: Record<string, unknown> // request payload
output?: unknown // Action result, available in after
user?: { user_id, metadata } // current user
studio?: { studio_id, status } // current studio
service?: { id, name } // current Service
action?: { id } // current Action
}