Gas & Wallet Economics

What this skill covers

How token economics work on Tawa — pod pricing, the deploy gate, gas metering, and wallet management.

Token Basics

  • 1 gas token = $0.01 USD
  • New accounts receive 50,000 free intro tokens
  • Tokens pay for two things: hosting (pod uptime) and transactions (API calls)
  • Token spend order: intro tokens first, then purchased, then earned

Pod Tiers and Pricing

TierCPUMemoryGas/HourGas/MonthUSD/Month
nano0.25256Mi53,600$36
small0.5512Mi107,200$72
medium11Gi2316,560$165.60
large22Gi4230,240$302.40
xlarge44Gi8359,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).

TierCPU requestCPU limitMemory requestMemory limit
nano25m250m128Mi256Mi
small50m500m256Mi512Mi
medium100m1512Mi1Gi
large200m21Gi2Gi
xlarge400m42Gi4Gi

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-tier and redeploy. Before this enforcement (builder < 0.6.x) every pod got a flat 500m/512Mi regardless of tier; if a service was silently relying on that, pick the tier that matches its real need (small = the old default).

Deploy Gate

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:

  • Org allowlist — spec.owner must be in the approved-orgs list (ALLOWED_DEPLOY_ORGS, default insureco).
  • Owner tamper guard — spec.owner must match the registered service owner, so a deploy can't charge another org's wallet.

Platform Services Skip the Gate

These services bypass the deploy gate: iec-wallet, iec-janus, tawa-web, koko, bio-id, iec-test.

Gate is Fail-Open

If the wallet service is unreachable, deploys proceed anyway (resilience over strictness).

Gas Metering via Janus

Janus tracks two types of gas events:

TypeCharged ForExample
hostingPod uptime per hourRunning my-api on nano tier
transactionAPI calls through KokoGET /api/users (5 gas)

Transaction gas is only charged on successful (2xx) responses.

Only Janus moves money (locked)

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:

MoverWhat it chargesWhere
Janus request meteringper-call route gas (public gateway and /i/)iec-janus gas middleware
Janus hosting workerpod-tier and storage gas, hourlyiec-janus src/queues/hosting-worker.ts
iec-cron5 gas per schedule fireiec-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 /i path short-circuited the /i/ gas check, so no /i/ call ever debited a wallet or wrote a gas_events row. The earlier statements here that /i/ "charges a flat 1 gas" were wrong in practice. /i/ now charges the target route's declared gas and honours its charge; the rest of this section describes that behaviour.

What you do

  1. Price your routes in spec.routes: gas (tokens per successful call, 0 = free) and charge (who pays: service, the default, or callerOrg).
  2. Call other services through Janus /i/ using the injected {SERVICE}_URL:
    • Forward the end user's token as X-Forward-User when the user's org should pay (pass-through billing).
    • Send only your own service token when your org should pay.
  3. Handle 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.

What you never do

  • Never call iec-wallet to debit or credit — not /debit, not /credit, not admin-credit, not wallet creation with initialTokens, whatever the SDK allows.
  • Never meter gas yourself — no counters, no "charge after the fact", no batching usage into a later wallet call.
  • Never hold wallet credentials — no wallet API key, no internal key, no admin token in tawa config. If a service has one, that is a bug to remove, not a secret to rotate.
  • Never price a call in code. The price lives in the catalog, where Janus reads it.

Reading your own balance or ledger for display is fine. Moving money is not.

How a layered call chain is paid

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
  • User-paid cost: a hop whose target route is 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.
  • Service-paid cost: a hop called without 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).
  • Price lookup. Janus prices an /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.
  • A route that does not declare 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.
  • A valid 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.
  • Fail closed. If route prices cannot be loaded from Koko (boot window, outage), a third-party call gets 503 proxy_gas_unavailable (recorded route_price_unavailable); platform → platform calls stay free.
  • Relay advice. Email and SMS over /i/relay/send are free until relay declares gas on its route. Declare it there; no caller change is needed.

Who pays: the payer table (4 Oct 2026)

Janus decides the payer for every priced call before forwarding it. Checked in this order:

Payer (recorded as)WhenWalletIf 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 sendswallet-insurecoNever 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 exist402; an unknown org is 422
Forwarded user (forwarded_user)Valid X-Forward-User and the route is charge: callerOrg or declares no chargethe user's org402
Calling service (calling_service)No forwarded org, or charge: servicethe calling service's org402
Freegas: 0, undeclared route, or platform service to platform-owned target with no forwarded org (platform_internal)nonen/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 routes SEPTOR_URL through 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.

Daily gas check

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.

AlertMeans
PRICED_TRAFFIC_NOT_CHARGEDBillable priced calls, none charged
CHARGES_STOPPED / CHARGES_DROPPEDCharged calls fell to zero, or below 20% of the trailing 7-day median
LEDGER_MISSING / LEDGER_DIVERGENCEJanus's charged count disagrees with iec-wallet's distinct debit count
SETTLEMENT_FAILURESSettlement 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.

Known platform gaps (verified 2026-10-04)

The rule is locked; the enforcement is nearly finished. Two gaps remain in how Janus charges a catalog's declared prices:

GapWhat happens todayIssue
charge is not carried by the builderThe 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 JanusServices 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 a gas_events row. It now charges the target route's declared gas.
  • 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-/api routes forward their full path (septor's /v1/events/emit reaches 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.

Wallet Operations

# Check your balance
tawa wallet

# Buy more tokens
tawa wallet buy 10000

# View gas spending
tawa gas

Key Facts

  • Start with nano tier — it's enough for most services and keeps gas costs low
  • The 50,000 intro tokens cover ~13 months of a nano pod
  • Deploy gate checks happen BEFORE the Docker build (saves time on failures)
  • Gas events are scoped by org — each org sees only their own usage
  • Pod tier is the single biggest factor in your monthly cost
  • Only Janus moves money (locked). Price routes in spec.routes; never debit, credit or meter in your own code
  • Route params are {name} or :name, and the request method picks the route (and price) within a service; see the catalog reference
  • Janus checks yesterday's charges every day and alerts when priced traffic goes uncharged

Last updated: October 6, 2026