Architecture
Cheela orchestrates. Your infrastructure executes. Almost everything surprising about the API follows from that one division.
The split
Cheela holds the model credential, the agent loop, the quota, and the trace history. You hold the capability code, the database, and your users’ identities. Neither side has the other’s secrets.
your app / widget a stranger's agent
│ │
│ POST /v1/runtime/execute │ POST /v1/capabilities/:rt/:cap
│ (public key, ch_pk_) │ (no auth — from your manifest)
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ Cheela control plane │
│ agent loop · quota · traces · analytics · manifest │
└──────────────────────────────────────────────────────────┘
│ │
│ model call │ signed HTTPS
│ (Cheela's OpenRouter credential) │ x-cheela-signature
▼ ▼
model provider your endpoint
└─ your handler, your databaseThe request path
A chat message arriving at POST /v1/runtime/execute goes through this, in order:
- Authenticate. The runtime’s public key is matched to a runtime record. The runtime id comes from that record, never from the request body — so a caller can only ever execute the one runtime whose key they hold.
- Check the origin and the rate limit. If the runtime has an allowlist, the browser’s
Originmust be on it. The limiter buckets against the runtime, not the caller’s IP. - Check quota. Once, on entry. The owner’s tier sets the ceiling.
- Call the model with your deployed capabilities attached as tools.
- Dispatch tool calls. For each one, Cheela signs an HTTPS request and sends it to your endpoint. Results are appended to the transcript.
- Repeat from step 4 until the model stops calling tools, or the step budget runs out.
- Record. Messages, token counts, capability calls and duration land in the trace.
Steps 4 to 6 are the agent loop, and it is bounded — see Executions for the step budget and what one execution costs.
Send Accept: text/event-stream and the same execution arrives as server-sent events instead of one JSON body. Without that header the response is byte-for-byte what it has always been.
The public broker
The second entry point exists because of the manifest. When you publish an Agent Discovery Specification document, every capability address in it points at POST /v1/capabilities/:runtimeId/:capability — Cheela’s broker, never at your own endpoint, which only accepts signed requests from Cheela.
That path has no model and no loop. The caller has already decided what it wants and supplies the input directly. It is also unauthenticated, deliberately: the manifest is public, so requiring a credential to call what you have published the schema for would advertise a door nobody can open.
Anonymous broker calls spend the owner’s quota, so they draw on a smaller sub-allowance as well as the main one — traffic against your public manifest cannot starve your own widget. Capabilities marked requiresEndUser are refused on this path before anything is metered.
Four credentials
Confusing two of these is the most common setup failure, so they have distinguishable prefixes.
| CREDENTIAL | SHAPE | HELD BY | AUTHORISES |
|---|---|---|---|
| Deploy key | ch_sk_… | Your CI, your shell | POST /v1/deployments, runtime status, heartbeat |
| Public key | ch_pk_… | Your page source, publicly | POST /v1/runtime/execute and nothing else |
| Runtime secret | opaque | Your endpoint | Verifying that a request really came from Cheela |
| End-user token | whatever you issue | Your user’s browser | Nothing, to Cheela. Your handler decides what it means. |
The first two used to be one key, which meant anyone who viewed a page’s source could push a deployment and overwrite the runtime’s capability set. Keep them apart.
Why requests are signed
Your capability endpoint is on the public internet. Without proof of origin, anyone who learns its URL can run your capabilities. So Cheela signs every call:
HMAC-SHA256(runtimeSecret, "runtimeId.timestamp.nonce.sha256(body)")Sent as four headers:
| HEADER | CONTENTS |
|---|---|
| x-cheela-runtime-id | The runtime this call is for |
| x-cheela-timestamp | Milliseconds since the epoch |
| x-cheela-nonce | Single-use value, per request |
| x-cheela-signature | The HMAC, hex-encoded |
createCheelaHandler checks them in a deliberate order: headers present, runtime matches, timestamp inside a five-minute tolerance, nonce unused, signature matches in constant time. The cheap structural checks run first so malformed traffic is rejected before any HMAC work happens, and the nonce is claimed after the signature verifies so an attacker cannot burn nonces with forged requests.
What the signature does not do
It proves integrity and origin. It does not provide confidentiality — capability inputs, outputs, and the end user’s credential travel in the request body. That is why endpoint must be https://, with an exception only for localhost.
What follows from this
- You cannot choose a model. Executions run on Cheela’s central OpenRouter credential so tokens can be metered and billed.
cheela.config.tshas noproviderormodelfield, and the registration API rejects one rather than accepting it and silently dropping it. - Cheela never sees your users. An end-user credential is forwarded to your handler untouched and is deliberately kept out of
metadata, because metadata is recorded in traces and a credential must not be. - A capability that was never deployed cannot be called. The manifest is built from the stored deployment, not from anything local, so over-advertising has no path.
- Your endpoint must be reachable from the internet. During development that means a tunnel.