Companion to: CIP-15 v2: Public Asset Hosting. This is not a normative CIP; it is an implementation handbook.
Audience: Gateway implementers
Status: Living document
Updated: 2026-05-11 (r2)
0. Mental model
The Gateway is a stateless HTTP server with two caches:- Resolves the actor from the host header (CIP-14 Route Registry — already implemented).
- Looks up the cached
Routesfor that actor. - Picks a
(verb, path)winner (CIP-15 §6.6). - Dispatches: serve from a CBFS volume (static), or invoke a named handler via existing CIP-14 RPC (dynamic). Optionally gates on payment per CIP-18.
1. Build order
Implement in this order. Each phase is independently shippable.
P1 alone replaces the current “everything goes to
http.request” CIP-14 behavior with verb-aware routing. Worth shipping that first and proving the core loop end-to-end before tackling static.
2. Phase 1 — Routes fetch + method dispatch
2.1 New types
2.2 New RPC: GET_STATE
Status (r2): not yet specified by any CIP, not yet implemented. The previous draft of this section pointed to a “CIP-15 §8.12” that does not exist in CIP-15 v2. Inspection of node/rpc/src/rpc.rs:168-213 confirms the routes today are only:
/actor/{address}— committed-state read of actor code + storage + manifest (no Merkle proof)/actors/{address}/storage— paginated KV read (no Merkle proof)- read-only handler invocation:
read_handlerper CIP-14 v2.r2 Part II §5 (also not yet implemented)
- Coordinate with the runtime team. Either start a sibling CIP (e.g. an extension to CIP-14 v2 or a fresh
CIP-15.1 / CIP-17: Verifiable State Read), or getGET_STATEadded to the CIP-14 v2 Part II §5 read-handler RPC family. It is cheap — a single Merkle path proof against the actor’s state root, leveraging the same MPT primitives CIP-4 describes. - Track this as a hard precondition for shipping Phase 1.
GET_STATE lands, you can prototype with an unverified read against a trusted Runner — but do NOT ship without the proof check. The whole security model rests on it.
2.3 Routes fetch loop
- The poll interval is shared with the volume
manifest_rootpoll — one polling task can refresh both anchors per actor. - Empty value (actor never wrote
__cowboy/routes) → not an error. Cache an empty Routes; dispatch falls back to CIP-14 unchanged. Existing actors continue to work. - Failures are sticky in the worst sense: if validation fails, the previously-cached table stays in use. That’s the safe behavior.
2.4 The resolver
This is the function that gets called on every request:{name} (single segment, captured), and *name / * (zero+ segments, captured if named).
2.5 Dispatch — Method target, pays = actor
This is the “easy” path for P1: it reuses CIP-14 dispatch end-to-end. The only change is the handler name comes from the matched route, not always "http.request":
http.request. Now the Gateway sends to route.target.name. Confirm with the runtime team that named-handler dispatch on the CIP-14 query and command paths is supported. (It already should be — that’s how non-HTTP message handlers work.)
2.6 Tests for P1
Unit:- Resolver: every branch in CIP-15 §6.6 — priority ties, longest-prefix ties, ANY vs concrete verb, Method-vs-Volume tie, empty routes table, reserved paths.
- Validation: malformed CBOR, oversized table, unknown volume, missing price for
caller, invalid status codes, reserved path prefix. - Path matching: literals,
{id},*rest, edge cases (trailing slashes, empty segments, encoded chars).
- Deploy a sample actor with two routes:
GET /a → method_a,POST /b → method_b. curl GET /a→ handlermethod_aruns.curl POST /b→ handlermethod_bruns.curl GET /unknown→ 404.- Mutate the routes table at runtime, wait one poll interval, verify dispatch follows the new table.
- Have the actor return zero routes; verify CIP-14 fallback (everything hits
http.request).
- One real actor, one Gateway, one Runner. Run the resolver under load (e.g.,
wrk -t4 -c100 -d30s http://actor.cowboy.network/api/foo). Watch for cache miss thrashing if the poll interval is wrong.
3. Phase 2 — Static volume serving (Volume targets)
The static-asset serving path is independent of the routes-resolver — once aVolume target wins resolution, the Gateway’s job is just to fetch and serve the object.
3.1 Components
- Per-volume metadata cache (CIP-15 §8.2 Layer 1a): manifest, content_types, cache_config, cors_config.
- Object cache (Layer 2): bounded LRU, keyed by
(volume_id, object_path). manifest_rootpolling for invalidation (CIP-15 §8.3).GET_MANIFEST+GET_SHARDRPCs (CIP-15 §8.4).- Reed-Solomon reconstruction + BLAKE3 integrity chain (CIP-15 §8.5–§8.6).
- Hedged parallel fetch (CIP-15 §8.7).
3.2 Volume-target dispatch
get_object is the existing P2 plumbing — cache check, manifest lookup, parallel shard fetch, RS reconstruct, integrity verify. No changes needed there.
3.3 Tests for P2
- Volume mount serves from the right
volume_nameand appliesstrip_prefix/volume_path_prefixcorrectly. - Fallback path (SPA case):
GET /aboutwith noaboutobject → servesindex.htmlwith status 200. - 404 fallback:
GET /missing.pngwith no fallback configured → 404. - 405 on
POST /assets/foo.png(volume route, non-GET). - Integrity chain: corrupted shard → fetch from another node and verify; if all corrupt → 502.
4. Phase 3 — Runtime mutability
This is mostly free once P1 is in. The poll loop already detects state_root changes and reloads. Two extra concerns:4.1 Rate-limit enforcement
The runtime — not the Gateway — is intended to enforceMIN_ROUTES_UPDATE_INTERVAL_BLOCKS. Status (2026-07): not yet implemented — the deployed UpdateRouteManifest handler (node/execution/src/execution/system_instruction.rs) and route_manifest validation (node/ras/src/route_manifest.rs) validate structure only and do not rate-limit updates per block; there is no RoutesUpdateRateLimited error variant. Until implemented, the Gateway only sees the result (the next valid state_root). But the Gateway should:
- Log routes-table churn (rate of state_root changes per actor) for ops visibility.
- Treat rapid back-to-back updates as expected noise; don’t add Gateway-side rate limits.
4.2 Validation-rejected updates
If an actor writes a malformed routes value:- Validation fails (§6.8).
- The Gateway logs a warning, keeps the previously-cached table, and continues serving from it.
- Next poll: if the actor fixes it, replace. If not, keep serving stale.
4.3 SDK helpers vs wholesale rewrites
The Gateway doesn’t distinguish: any write that advances the state_root and changes__cowboy/routes is just “a new table.” The narrow mutators (§6.9) are an SDK convenience. From the Gateway’s perspective they’re all the same.
5. Phase 4 — pays = caller (CIP-18)
CIP-18 may not be ready when this CIP ships. That’s fine: the schema lands now, enforcement waits. Two options:
Option A: Accept the schema, reject the requests. If a route has pays = "caller" and CIP-18 isn’t implemented, the Gateway returns 501 Not Implemented with X-Cowboy-Reason: cip18-required. Document this clearly so actor authors don’t ship paid endpoints expecting them to work.
Option B: Validate-only. The Gateway accepts schemas with pays = "caller" but treats them as if they were pays = "actor" until CIP-18 lands. This is more forgiving but lets actors ship endpoints that quietly behave differently than declared. Don’t do this.
Once CIP-18 lands, the Method-target dispatch path adds:
6. Phase 5 — CORS, conditional requests, compression
Per CIP-15 §9 / §8.9 / §8.10. Note the precedence rule in §9.4: forMethod-target routes, actor-set Access-Control-* headers in the response envelope are authoritative — the Gateway only fills in CORS from cors_config when the actor sets none. For Volume-target routes, the Gateway applies cors_config rules; the default permissive CORS policy applies when no _meta/cors.json exists.
7. Phase 6 — Optimization
After correctness:- Hedged parallel shard fetch (CIP-15 §8.7).
- Compressed variants in the cache.
- Cache warming on actor registration.
- Coalescing concurrent requests for the same object.
8. Phase 7 — Workload streaming routes (WS/SSE)
Per CIP-15 §8.15. Initial shape: operator static Gateway configuration binds(verb, path) → workload registry record; no chain surface, so this phase has no dependency on the v2 route-manifest rewrite.
- Config loader: static workload routes per the
StaticWorkloadRouteschema (CIP-15 §8.15) — keyed(actor, verb, path), host resolved to actor before lookup; collision with the current actor manifest fails config load, and every manifest refresh rechecks overlap so a later actor route immediately disables the conflicting static entry for that manifest generation. - Endpoint resolution: workload record id →
serving_endpointvia the workload registry record; validateowning_actor == entry.actor(or, when set,owning_actor == expected_workload_owner). Never accept an endpoint from request data. - SSE relay: streamed responses flush unbuffered;
idle_timeout_mson response-chunk silence;WORKLOAD_CONNECT_TIMEOUT_MSon establishment; cancellation and permit release when the client drops the body. - WS relay:
auth/paysgating and backend handshake before the client101; propagate backend rejection and selected subprotocol; disable extensions in the initial operator-configured deployment; then relay data/close semantics in both directions, handling ping/pong hop-locally while counting them as activity, with backend death → client close1011. - Caps: route
max_concurrent_streams(defaultDEFAULT_MAX_CONCURRENT_STREAMS), acquired before registry/dial work and held through body/WS teardown;503beyond, no queueing. - Tests: SSE event flush latency (no whole-body buffering), idle teardown, client-drop cancellation/permit release, concurrent-cap rejection at bound + zero/ceiling validation, connection-lifetime cap including handshake time, mid-stream backend kill emits close
1011, backend handshake401propagation, subprotocol selection and extension stripping, WS Origin passthrough, 402-gated establishment, owner/actor binding enforcement (mismatched record rejected;expected_workload_ownerhonored and logged; record-swap negative — override no longer matching the repointed record’s owner fails closed), semantic static-vs-actor collision rejected at load and disabled after a conflicting manifest refresh (wildcard, param, andANYoverlap vectors, not just equal keys), cross-target auth replay (Runner-signed request rejected on a Workload route and vice versa — distinct domain tags) plus golden vectors for the pinned"cowboy/cip15/workload-route-auth/v1"domain, workload-id encoding validation (length + noncanonical hex rejected), path/query/header forwarding and hop-by-hop stripping.
9. Operational surface
What ops needs to see:- Per-Gateway: cache hit ratio (object, metadata, routes), cache size, evictions.
- Per-actor: requests/s, dispatch breakdown (volume vs method, by verb), routes-table state_root churn rate.
- Per-route: hit count, p50/p95/p99 latency.
- Per workload route: open connections vs cap, connection duration histogram, idle vs backend-death vs client-close teardown counts.
- Errors: validation failures (per actor), proof failures, 502s from integrity failures.
X-Cowboy-Source header ("static" or "dynamic") on responses already gives clients a debugging signal.
10. Open questions (confirm before merging the PR)
GET_STATERPC. Confirmed (r2) as not specified and not implemented — see §2.2. Either start a sibling CIP or extend CIP-14 v2 Part II §5read_handlerfamily. Blocks Phase 1 shipping with full proof-checked routes cache.- Path-param delivery. This CIP specifies the Gateway extracts
path_paramsand passes them in theHttpRequestEnvelope. Confirm with the runtime team that the envelope schema can carry them, or extend it. - Named-handler dispatch. Confirm the runner-side dispatcher accepts arbitrary handler names on the CIP-14 query and command paths (it should — that’s how non-HTTP messages work — but verify).
- CIP-18 readiness. r2 elevates CIP-18 from Companion to Requires for Phase 4 — track CIP-18 PaymentGate
0x12deployment. Ship Phase 4 only after CIP-18 settles. - Rollout. Is there an existing actor that’s a good first canary?
17-hn-feed? A toy actor in a devnet?
11. What success looks like
- Existing actors keep working with zero changes.
- Actors that write a
__cowboy/routesvalue get verb-aware dispatch + named handlers + per-route payment policy without redeploys. - Static asset serving meets CDN-class latency for cache hits (sub-10ms p99 from a warm Gateway).
- Routes updates land in Gateway caches within 6 seconds of commit.
- A FastAPI/axum/Hono developer can read a CIP-15 actor and immediately understand what it does.

