@meterbox/entitlement-sidecar
v0.1.0
Published
In-process entitlement cache for Meterbox: sub-millisecond checks that survive API downtime via stale-while-error.
Maintainers
Readme
@meterbox/entitlement-sidecar
In-process entitlement cache for Meterbox. Run it
inside your API gateway / app server / ingress and call check() on every
request. Answers come from a local Map in sub-millisecond time, with
stale-while-error keeping you live during Meterbox outages.
This is a thin wrapper around GET /v1/entitlements/:id. It exists so you
can put gating in the request hot path without paying the network round-trip
on every call, and without falling open when Meterbox is briefly
unreachable.
Install
npm install @meterbox/entitlement-sidecarNode 18+ required (uses the global fetch).
Use
const { EntitlementSidecar } = require("@meterbox/entitlement-sidecar");
const sidecar = new EntitlementSidecar({
baseUrl: "https://meterbox.ai", // or your self-hosted Meterbox host
apiKey: process.env.METERBOX_API_KEY, // mb_live_... from Settings > API keys
// ttlMs: 30_000, // re-fetch after 30s (default)
// staleWhileErrorMs: 300_000, // serve cached for up to 5min on API failure
});
// In your request handler:
const decision = await sidecar.check({
customer_id: "cus_42",
model_id: "llm-smart",
estimated_input_tokens: 1500,
});
if (!decision.allowed) {
return res.status(402).json({ error: decision.reason, detail: decision.detail });
}The response is the exact shape GET /v1/entitlements/:id returns, plus three
cache-status flags:
{
"allowed": true,
"pct_used": 18.4,
"cost_estimate_usd": 0.006736,
// ↓ added by the sidecar
"cached": true, // served from the local cache (vs freshly fetched)
"stale": false, // entry was past ttlMs (kept alive by stale-while-error)
"age_ms": 412, // how old the served entry is
}Invalidate on webhooks
For instant updates after a plan change or status flip, subscribe your app
to Meterbox's outbound webhooks and call invalidate():
app.post("/meterbox/webhook", (req, res) => {
// ...verify Meterbox-Signature against your stored secret...
const { type, data } = req.body;
if (type === "customer.status.changed" || type === "entitlement.changed") {
sidecar.invalidate(data.customer_id);
}
res.status(204).end();
});Without invalidate(), the next check still picks up the new state once the
cached entry ages past ttlMs, trading some staleness for fewer webhook
moving parts.
Pre-warm at boot
If you know the customer ids that will hit your service immediately after startup (a small B2B tenant list, for example), pre-fetch them so the first per-customer request doesn't pay the round-trip:
await sidecar.prewarm(["cus_alpha", "cus_beta", "cus_gamma"]);
// → { ok: 3, failed: 0 }Configuration reference
| Option | Default | Notes |
| ------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| baseUrl | required | Your Meterbox host, e.g. https://meterbox.ai |
| apiKey | required* | Tenant API key (mb_live_...), sent as Authorization: Bearer. Scoped to one environment (sandbox or production) |
| adminKey | | *Instead of apiKey on a self-hosted deployment: the operator key, sent as X-Admin-Key. A tenant key passed here is still sent as Bearer |
| ttlMs | 30_000 | After this many ms, the next check() re-fetches |
| staleWhileErrorMs | 300_000 | When Meterbox is unreachable, serve the cached entry up to this age before re-throwing |
| requestTimeoutMs | 5_000 | Per-fetch timeout, then fall back to stale (if any) |
| fetch | globalThis.fetch | Injectable for tests / non-global-fetch envs |
| logger | () => {} | Called as logger(level, message, meta) on stale-fallback events |
When NOT to use this
- If your tenant runs a non-Node stack, call
GET /v1/entitlements/:iddirectly. Meterbox's data plane caches the answer in-pod, so the server-side cost is already sub-millisecond. The gain from this SDK is saving the network round-trip and the stale-while-error fallback. - If you can carry the full entitlement state in a short-lived JWT (see
POST /v1/tokens/:idon the Meterbox side), and your gating doesn't need live quota counters, the JWT is even cheaper because verification has zero round-trips. The SDK is the right pick when you want live counters / status flips reflected without re-issuing tokens.
