How token economics work on Tawa — pod pricing, the deploy gate, gas metering, and wallet management.
| Tier | CPU | Memory | Gas/Hour | Gas/Month | USD/Month |
|---|---|---|---|---|---|
| nano | 0.25 | 256Mi | 5 | 3,600 | $36 |
| small | 0.5 | 512Mi | 10 | 7,200 | $72 |
| medium | 1 | 1Gi | 23 | 16,560 | $165.60 |
| large | 2 | 2Gi | 42 | 30,240 | $302.40 |
| xlarge | 4 | 4Gi | 83 | 59,760 | $597.60 |
Set pod tier in catalog-info.yaml:
metadata:
annotations:
insureco.io/pod-tier: "nano" # Default if not specified
The tier is enforced as real container resources. On deploy the builder sets
the pod's Kubernetes resources.limits to the tier's CPU/Memory above. This means
the size you pick is the size you get — the tier's gas rate and its actual
CPU/Memory ceiling are the same knob. Bumping the tier raises both the cost and
the pod's real ceiling; lowering it does the reverse. An unknown tier falls back
to nano with a build warning. The namespace LimitRange max is 4 CPU / 4Gi
(xlarge), so no tier is capped below its declared size.
Requests are lower than limits, and CPU and memory are treated differently. Memory requests are 50% of the limit. CPU requests are 10% of the limit (builder ≥ 2026-09-07; they were 50% before).
| Tier | CPU request | CPU limit | Memory request | Memory limit |
|---|---|---|---|---|
| nano | 25m | 250m | 128Mi | 256Mi |
| small | 50m | 500m | 256Mi | 512Mi |
| medium | 100m | 1 | 512Mi | 1Gi |
| large | 200m | 2 | 1Gi | 2Gi |
| xlarge | 400m | 4 | 2Gi | 4Gi |
Why the split: memory is not compressible, so what a pod needs must be reserved
for it, and measured fleet usage sits close to the memory requests. CPU is
compressible: the request only decides where a pod is scheduled and its share if
a node is ever saturated, while the limit still decides how far it can burst. At
50% CPU requests the cluster ran 91% reserved and 9% used, deploys sat
Pending on Insufficient cpu beside eight nearly idle nodes, and the autoscaler
added machines to hold reservations nothing consumed. Nothing about a pod's
behaviour changes under the lower request until a node is genuinely CPU-bound.
The new requests apply on each service's next deploy; a running pod keeps the
values it was deployed with. A pod that is actually slow or throttled is still
under-tiered — the fix is the same as before: raise pod-tier and redeploy.
A service can move its requests off these defaults without changing tier, limits
or gas: insureco.io/memory-request and insureco.io/cpu-request (floor 64Mi / 10m,
ceiling the tier's limit, invalid values warn and fall back). Example: iec-test runs
xlarge for its 4Gi ceiling with memory-request: "512Mi". See
catalog-info.
A pod stuck slow/CPU-throttled is usually under-tiered — raise
pod-tierand redeploy. Before this enforcement (builder < 0.6.x) every pod got a flat500m/512Miregardless of tier; if a service was silently relying on that, pick the tier that matches its real need (small= the old default).
Before every build, the builder checks your wallet has enough gas to cover 3 months of hosting:
Required reserve = hostingGasPerHour × 24 × 30 × 3
For a nano pod: 5 × 24 × 30 × 3 = 10,800 tokens ($108)
If you also declare spec.storage, the gate adds 3 months of storage gas on top:
Required reserve = (hostingGasPerHour × 24 × 30 × 3) + (storageGasPerMonth × 3)
If your wallet balance is below the reserve, the deploy is blocked with:
"Deploy gate: insufficient gas reserve"
The deploy gate runs two more hard-fail checks before the reserve check:
spec.owner must be in the approved-orgs list (ALLOWED_DEPLOY_ORGS, default insureco).spec.owner must match the registered service owner, so a deploy can't charge another org's wallet.These services bypass the deploy gate: iec-wallet, iec-janus, tawa-web, koko, bio-id, iec-test.
If the wallet service is unreachable, deploys proceed anyway (resilience over strictness).
Janus tracks two types of gas events:
| Type | Charged For | Example |
|---|---|---|
| hosting | Pod uptime per hour | Running my-api on nano tier |
| transaction | API calls through Koko | GET /api/users (5 gas) |
Transaction gas is only charged on successful (2xx) responses.
Locked platform rule — decided 2026-09-24. Not a recommendation, and not open to a per-service exception.
Janus is the only thing that debits or credits a wallet. A service declares what its
routes cost in catalog-info.yaml spec.routes; Janus charges it. The only other code allowed
to move money is the platform's own billing jobs:
| Mover | What it charges | Where |
|---|---|---|
| Janus request metering | per-call route gas (public gateway and /i/) | iec-janus gas middleware |
| Janus hosting worker | pod-tier and storage gas, hourly | iec-janus src/queues/hosting-worker.ts |
| iec-cron | 5 gas per schedule fire | iec-cron src/scheduler/manager.ts (GAS_PER_RUN = 5) |
Everything else — every app, plugin, worker and agent — moves money only by making a call through Janus.
Correction (2026-10-04):
/i/was never metered before this change (iec-janus#10). A global free-infrastructure marker on the/ipath short-circuited the/i/gas check, so no/i/call ever debited a wallet or wrote agas_eventsrow. The earlier statements here that/i/"charges a flat 1 gas" were wrong in practice./i/now charges the target route's declaredgasand honours itscharge; the rest of this section describes that behaviour.
spec.routes: gas (tokens per successful call, 0 = free) and
charge (who pays: service, the default, or callerOrg)./i/ using the injected {SERVICE}_URL:
X-Forward-User when the user's org should pay
(pass-through billing).401 (proxy_forward_user_required) as "this route bills the user's org and you
did not forward their token", 402 (proxy_insufficient_gas) as "the payer is out of
gas", not as a bug, and
503 (proxy_gas_unavailable) as "Janus could not meter this call" — it fails closed and
forwards nothing. A platform-owned service client with no X-Forward-User is billed to the
InsurEco system wallet; a first-time caller gets a wallet created and then the usual order
applies (promo, purchased, 402). See Who pays in Service-to-Service Communication./debit, not /credit, not
admin-credit, not wallet creation with initialTokens, whatever the SDK allows.tawa config. If a service has one, that is a bug to remove, not a secret to rotate.Reading your own balance or ledger for display is fine. Moving money is not.
Each hop is priced by the route it lands on, and paid by whoever that route and the call say pays. Janus charges each hop separately.
user ──► A POST /api/a/quote (gas: 5, charge: callerOrg) user's org pays 5
│
├─► /i/b/api/rate X-Forward-User: <user token>
│ B route: gas 3, charge: callerOrg user's org pays 3
│
└─► /i/c/api/log service token only
C route: gas 1, charge: service A's org pays 1
charge: callerOrg, called with the user's
token forwarded. The user's org pays every such hop, so the user's total is the sum along
the chain.X-Forward-User, or to a charge: service
route. The calling service's org pays. That is how a service absorbs a cost it chooses
not to pass on (freemium, internal tooling, its own cron work)./i/ call by the route the target actually receives: the
path after /i/{service}, query string dropped, matched against that service's routes in
Koko, method-aware. A literal path match wins; otherwise a route registered with stripPrefix
is matched with its prefix removed. There are no gateway fallbacks and no default price.gas, an undeclared path, or gas: 0 costs nothing on
/i/. No wallet is touched; Janus records the call in gas_events with charged: false
and a reason (zero_price, undeclared_route). An ambiguous stripPrefix match (several
candidate routes with different prices) is treated as undeclared and logged.charge: callerOrg on a priced route requires X-Forward-User. Without it the call is
refused 401 proxy_forward_user_required and nothing is forwarded. A free route never needs
the header. With charge: service, the calling service pays even if a user token is sent;
with no charge declared, a valid X-Forward-User org pays, else the calling service.X-Forward-User bills the user's org even when the caller is a platform-owned
service. Platform service → platform-owned target with no user token is free, recorded
with the shadow price and charged: false, reason platform_internal.503 proxy_gas_unavailable (recorded route_price_unavailable);
platform → platform calls stay free./i/relay/send are free until relay declares gas on
its route. Declare it there; no caller change is needed.Janus decides the payer for every priced call before forwarding it. Checked in this order:
| Payer (recorded as) | When | Wallet | If it has no gas |
|---|---|---|---|
System (system) | The route is system-paid: charge: system on a platform-owned route, or listed in the gas.systemPaidRoutes setting. Wins over everything else the caller sends | wallet-insureco | Never refused, no balance check. The debit is settled afterwards |
Bill org (bill_org) | A platform-owned service client sent X-Bill-Org: <orgSlug> (a cron callback billing an agency) | that org's wallet, which must already exist | 402; an unknown org is 422 |
Forwarded user (forwarded_user) | Valid X-Forward-User and the route is charge: callerOrg or declares no charge | the user's org | 402 |
Calling service (calling_service) | No forwarded org, or charge: service | the calling service's org | 402 |
| Free | gas: 0, undeclared route, or platform service to platform-owned target with no forwarded org (platform_internal) | none | n/a |
System-paid routes. charge: system is honoured only when the route's owner is the platform org
(PLATFORM_ORG_SLUG); a third party declaring it gets no effect, so no one else can bill InsurEco.
gas.systemPaidRoutes is an owner-scoped runtime setting, a list of { service, pathPrefix }, whose
default is [{ service: "septor", pathPrefix: "/v1/events/emit" }]. On a system-paid call the caller,
X-Forward-User and X-Bill-Org are all ignored, no agency wallet is touched, and the debit is a real one
(not the free platform_internal path). It settles through the retrying queue; if the final attempt fails,
Janus records charged: false with reason system_wallet_debit_failed and logs an error, and the call has
already been served. Janus's and iec-wallet's own audit emits stay unmetered.
Audit-event gas (decision, 4 Oct 2026, Derek). The InsurEco system wallet pays the gas for Septor audit events, not the service or org that emitted them, so a service is never billed for the audit trail of its own compliance events. At about 13,000 emits a day at 5 gas, that is roughly 65,000 gas a day from
wallet-insureco. This is not live yet. Services reach Septor directly over cluster DNS (SEPTOR_URL), which bypasses Janus, so the system-paid rule has nothing to bill until a builder change routesSEPTOR_URLthrough Janus. Until then audit emits are unmetered, as before.
X-Bill-Org (see Service Communication) is for platform-owned
callers only: a non-platform caller sending it gets 403 bill_org_not_permitted. Sending it together with
X-Forward-User is 400 payer_ambiguous. X-Bill-Org: insureco (the platform org) behaves as if it were absent.
Once a day, after 01:00 UTC, Janus checks the previous UTC day and alerts when priced traffic stops being
charged. It exists because of the 22 Sep failure, when traffic moved onto unmetered /i/ and no
gas_events existed to notice. It runs in-process (a 15-minute tick, a Redis lock so only one replica runs it)
and only when GAS_METERING_ENABLED=true.
| Alert | Means |
|---|---|
PRICED_TRAFFIC_NOT_CHARGED | Billable priced calls, none charged |
CHARGES_STOPPED / CHARGES_DROPPED | Charged calls fell to zero, or below 20% of the trailing 7-day median |
LEDGER_MISSING / LEDGER_DIVERGENCE | Janus's charged count disagrees with iec-wallet's distinct debit count |
SETTLEMENT_FAILURES | Settlement jobs exhausted their retries |
Results go out as Septor gas.daily_check.completed (always) and gas.daily_check.alert (on alert or a
failed check), an error log and Prometheus gauges. No email is sent. Platform admins read it at
GET /admin/gas/daily-check (stale: true if yesterday's check is overdue), /history, and can
re-run a day with POST /admin/gas/daily-check/run. Tuning is by GAS_DAILY_CHECK_* env vars on Janus.
The rule is locked; the enforcement is nearly finished. Two gaps remain in how Janus charges a catalog's declared prices:
| Gap | What happens today | Issue |
|---|---|---|
charge is not carried by the builder | The builder does not send charge to Koko (iec-builder#182), and Koko's route schema has no charge field (iec-koko#38). Janus reads charge when a route carries one, but none does in production yet: on the public gateway Janus bills the caller's org, and on /i/ a valid X-Forward-User org pays, else the calling service. charge: callerOrg and charge: system on a route have no effect until these land; system-paid routes work today only through the gas.systemPaidRoutes setting. | builder#182, koko#38 |
| Septor emits bypass Janus | Services call Septor directly (SEPTOR_URL is a cluster URL), so the system-paid audit-emit rule has nothing to bill. A builder change to route SEPTOR_URL through Janus is pending. | builder (pending) |
Correction (2026-10-04): three gaps were removed from this table.
/i/ignores route prices (iec-janus#10): fixed, and the old row was wrong about what it described./i/was never metered: a global free-infrastructure marker short-circuited its gas check, so it neither charged a flat 1 gas nor anything else, and no/i/call produced agas_eventsrow. It now charges the target route's declaredgas.- New routes need a Janus restart (iec-janus#11) and gateway routes forward
/(iec-janus#12): fixed by iec-janus PR #27, deployed 4 Oct 2026. Routes now dispatch per request from the live Koko cache, so a newly registered route works without a restart, and non-/apiroutes forward their full path (septor's/v1/events/emitreaches the pod as itself).
None of these is a reason to meter or debit in your own code. Declare the price and payer you
want; Janus charges the price today, and the payer once charge is carried. Wallet-side
enforcement (debit/credit restricted to Janus and the platform billing jobs) is tracked in
iec-wallet#12.
# Check your balance
tawa wallet
# Buy more tokens
tawa wallet buy 10000
# View gas spending
tawa gas
spec.routes; never debit, credit or meter in your own code{name} or :name, and the request method picks the route (and price) within a service; see the catalog referenceLast updated: October 6, 2026