Protect an API
Put Chio in front of Larkfield's pricing API, issue a quote under a grant, and revoke the grant.
On this page
Run a Chio sidecar in front of an HTTP API you already have, and the kernel decides each request before the API sees it. Callers present a capability for writes; the sidecar forwards what the kernel admits and signs a receipt for every decision.
The API keeps its own authentication, application behavior, and storage. Starts from chio new pricing-api --example pricing-api: Larkfield's pricing functions as a plain Python HTTP API with no Chio code in it. You put the sidecar in front, issue a quote for sku-301 under a five-minute grant to Basalt's buying agent, then revoke the grant and watch the next quote stop.
The sidecar decides before the upstream sees the request
The sidecar reads the API's OpenAPI document into a route table. Each request is matched to a route, then the kernel checks the capability the caller presents in the X-Chio-Capability header or the chio_capability query parameter.
κcapabilityσscopegguardsBbudget
A matched write forwards with its receipt
The proxy matches the method and path against its route table, then the kernel checks the capability in X-Chio-Capability. An allowed request is forwarded with that header stripped, and the response returns with its receipt id. HTTP Proxy.
An admitted request is forwarded without the X-Chio-Capability header, and the response returns with its receipt id. A refused one gets a structured 403 that names the missing input, and the upstream never sees it. The sidecar fails closed: when it cannot reach a decision, nothing is forwarded.
This boundary covers the requests that cross the sidecar. A caller that can reach the upstream port directly bypasses it, so close that port in the host, as the mediation boundary describes.
1. Start the pricing API
Pull the project, or download it. It holds the API, its OpenAPI document, a grant request, and a receipt reader.
chio new pricing-api --example pricing-api && cd pricing-api
python3 pricing_api.pyPricing API: http://127.0.0.1:8087; data: .pricing/quotes.db
Keep it running. It serves POST /v1/quote, GET /v1/quotes, and reads and withdrawals by quote ID, and it keeps issued quotes in .pricing/quotes.db.
Before you begin
You need the chio CLI, Python 3, and jq. Install the CLI with curl -fsSL https://chio.computer/install.sh | sh; Installation lists the other options.
2. Put the sidecar in front of it
In a second terminal in the same directory, start the sidecar. It reads openapi.yaml, forwards admitted requests to port 8087, and listens on port 9097.
chio --authority-seed-file .pricing/authority.hex api protect \
--upstream http://127.0.0.1:8087 \
--spec openapi.yaml --listen 127.0.0.1:9097 \
--receipt-store .pricing/receipts.dbThe CLI creates the signing seed in .pricing/authority.hex when it is absent and reuses it on restart. Receipts and revocations go to .pricing/receipts.db. From here on, callers talk to port 9097 only.
3. Ask for a quote without a capability
In a third terminal in the same directory, read the quotes, then ask for sku-301 at 80 percent.
curl -sS http://127.0.0.1:9097/v1/quotes
curl -sS -i http://127.0.0.1:9097/v1/quote \
-H 'Content-Type: application/json' \
-d '{"sku":"sku-301","percent":80}'{"quotes":[]}
HTTP/1.1 403 Forbidden
content-type: application/json
x-chio-receipt-id: 01a12ef0-bf17-7d80-ac22-c82d49abccb6
{"error":"chio_access_denied","message":"side-effect route requires a capability token","receipt_id":"01a12ef0-bf17-7d80-ac22-c82d49abccb6","suggestion":"provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter"}You should see an empty list, then 403 with chio_access_denied. POST /v1/quote is marked side-effecting, so it needs a capability. The body names the header to send and carries the receipt_id of the signed refusal; the API never saw the request.
4. Grant a capability and issue the quote
As the local operator, ask the sidecar for a five-minute grant for Basalt's buying agent. The request names a subject, a job, a lifetime, and the scope to issue.
{
"subject": "basalt-buyer",
"job_uid": "basalt-quotes-1",
"ttl_seconds": 300,
"scopes": ["tool:chio_http_authority:authorize_http_request:invoke"]
}Mint the capability into capability.json, then send the same request with it.
curl -sS http://127.0.0.1:9097/v1/capabilities/mint \
-H 'Content-Type: application/json' \
--data-binary @http-grant.json -o capability.json
curl -sS -i http://127.0.0.1:9097/v1/quote \
-H 'Content-Type: application/json' \
-H "X-Chio-Capability: $(jq -c .capability capability.json)" \
-d '{"sku":"sku-301","percent":80}'HTTP/1.1 201 Created
content-type: application/json
x-chio-receipt-id: 01a12ef0-bf64-7c12-b46e-430b507267c3
{"id":1,"sku":"sku-301","percent":80,"price":"28.00","currency":"USD"}You should see 201 and USD 28.00, with an x-chio-receipt-id header that names the HTTP receipt. The grant covers the sidecar's HTTP operation, authorize_http_request, for five minutes; it names neither the quote operation nor POST /v1/quote.
Your own client sends the same header, with the compact JSON of capability.json's capability field as its value.
const response = await fetch("http://127.0.0.1:9097/v1/quote", {
method: "POST",
headers: { "content-type": "application/json", "X-Chio-Capability": capabilityToken },
body: JSON.stringify({ sku: "sku-301", percent: 80 }),
});
console.log(response.status, response.headers.get("x-chio-receipt-id"));import httpx
response = httpx.post(
"http://127.0.0.1:9097/v1/quote",
headers={"X-Chio-Capability": capability_token},
json={"sku": "sku-301", "percent": 80},
)
print(response.status_code, response.headers["x-chio-receipt-id"])let response = reqwest::Client::new()
.post("http://127.0.0.1:9097/v1/quote")
.header("X-Chio-Capability", &capability_token)
.json(&serde_json::json!({ "sku": "sku-301", "percent": 80 }))
.send()
.await?;
println!("{} {:?}", response.status(), response.headers().get("x-chio-receipt-id"));The mint endpoint accepts operator requests on loopback. Before you expose a control endpoint, configure its bearer token and put it behind its own access boundary.
5. Confirm the quote and read its receipt
Read the quotes through the sidecar, then list the allowed decisions from the receipt store.
curl -sS http://127.0.0.1:9097/v1/quotes
python3 receipts.py --decision allow{"quotes":[{"id":1,"sku":"sku-301","percent":80.0,"price":"28.00","currency":"USD"}]}
01a12ef0-bf03-7a80-833e-f1276e427dc1 ALLOW GET /v1/quotes HTTP 200
01a12ef0-bf64-7c12-b46e-430b507267c3 ALLOW POST /v1/quote HTTP 201
01a12ef0-bf87-7c13-8e6f-4e5b177a5e6d ALLOW GET /v1/quotes HTTP 200You should see the recorded quote and one allow receipt per admitted request, reads included: GET needs no capability, and the sidecar still signs a receipt for it. Pass a receipt id to receipts.py to print the complete signed object. Receipts for HTTP calls shows that lookup and its verification.
6. Revoke the grant and confirm the write stops
Release the capability by its id, then present it again.
curl -sS http://127.0.0.1:9097/v1/capabilities/release \
-H 'Content-Type: application/json' \
-d "$(jq '{capability_id:.capability.id}' capability.json)"
curl -sS -i http://127.0.0.1:9097/v1/quote \
-H 'Content-Type: application/json' \
-H "X-Chio-Capability: $(jq -c .capability capability.json)" \
-d '{"sku":"sku-301","percent":80}'{"released":true}
HTTP/1.1 403 Forbidden
content-type: application/json
x-chio-receipt-id: 01a12ef0-c102-7d81-870e-b35ff90b8098
{"error":"chio_access_denied","message":"capability token has been revoked","receipt_id":"01a12ef0-c102-7d81-870e-b35ff90b8098","suggestion":"request a fresh capability token before retrying"}You should see {"released":true}, then 403 with capability token has been revoked and a receipt id. List the quotes again: the refused request recorded nothing. The store keeps revocations across sidecar restarts.
To issue new authority, change job_uid in http-grant.json before minting. An identical mint request derives the same capability id, and the revocation still applies to it.
The method decides whether a request needs a capability
The route table gives every operation a policy from its method, and the OpenAPI document can override it per operation with x-chio-side-effects.
| Methods | Default policy | Capability | Receipt |
|---|---|---|---|
GET, HEAD, OPTIONS | SessionAllow | Not required | Audit receipt |
POST, PUT, PATCH, DELETE | DenyByDefault | Required | Allow or deny receipt |
The sidecar signs a receipt either way. For named operation grants and a two-quote budget, serve the same API to an agent through the MCP host for the pricing API. Keep invocation accounting on a path that enforces it; the HTTP projection does not consume the presented token’s invocation or spending budget.