---
applyTo: "**"
title: "AFA Protocol skill for agent hosts"
version: 3.0
api: https://api.afaprotocol.com
---

# AFA Protocol skill for agent hosts

An agent acted at 03:12. Six weeks later someone asks who allowed it, under
what limits, and whether the record of it was changed afterwards. This skill
teaches an agent, and the person configuring one, to use AFA Protocol so that
question has an answer you can check yourself: a signed, ordered record of
what agents did and what they were allowed to do, kept by the hosted service
and, if you choose, by a ledger on your own machine.

Hosted API: `https://api.afaprotocol.com`. Console: `https://app.afaprotocol.com`.
Route reference with every request and response shape:
`https://app.afaprotocol.com/docs/`. Every REST call carries your key in the
`X-API-Key` header.

This document is a composed chain, not an essay: what AFA can tell you, the
surfaces, the first five minutes, the capability reference, the rules, and
worked examples. Each rule names the check that catches a violation. What it
costs is section 12.

An agent can fetch this document over the API it describes: `GET /v1/skill`,
or the `afa_skill` tool. No key is needed to read it.

## 1. What AFA can and cannot tell you

Limits first, because they are most of what you are buying.

AFA can tell you:

- that a record was not altered after it was written. Each entry is signed
  when it is written and linked to the one before it. Changing an old entry
  breaks the link, and the check that finds the break runs on your machine;
- that an action was checked against a grant or a policy before it ran, and
  what the answer was: `authorized`, `no_grant`, `expired`, `revoked`,
  `budget_exhausted`, `tool_not_allowed`, `invalid_expiry`, `invalid_limit`;
- in what order a delegation happened. A token signed for one order of
  principals does not verify under any other order;
- that two independent copies of the record agree, or exactly where they
  differ.

AFA cannot tell you:

- that an action was a good idea, that a policy was the right policy, or that
  the person who approved a step read it;
- what was said. The hosted record and the local ledger keep hashes of
  payloads, never the payloads. If you need the text, keep it yourself;
- who wrote an unsigned entry. An unsigned event still chains; it does not
  prove authorship. Register one signing key per machine and sign;
- anything while offline. Nothing customer-facing runs without the hosted
  API. The local ledger verifies locally, and its reconcile step needs the
  network.

Detection, not prevention. AFA does not stop a process from running a tool.
It refuses to record an action as authorized when no live grant covers it, and
it makes a later edit of the record visible. Your harness decides what to do
with a refusal.

## 2. The surface matrix

| Capability | REST | Hosted MCP tool | Console | Local operator tools |
|---|---|---|---|---|
| Health | GET /v1/health | afa_health | Start | no |
| Record an event | POST /v1/events, POST /v1/events/batch | afa_record_event | Records | no |
| Read events, tree, lineage, live stream | GET /v1/events/{event_id}, POST /v1/events/search, GET /v1/events/tree, GET /v1/events/lineage/{event_id}, GET /v1/events/stream | afa_event_get, afa_events_list, afa_event_lineage | Records, Activity | no |
| Settlement checkpoint and chain verify | POST /v1/chain/checkpoint, GET /v1/chain/blocks, POST /v1/chain/verify | afa_chain_verify | Records | no |
| Scoped cross-session dedup | POST /v1/dedup/check, POST /v1/policy/test, GET /v1/dedup/membership/{event_id} | afa_dedup_check | no | no |
| Sub-agent grants: create, status, use, revoke, list, containment | POST /v1/grants, GET /v1/grants/status, POST /v1/grants/{grant_id}/use, POST /v1/grants/{grant_id}/revoke, GET /v1/grants | afa_grant_create, afa_grant_status, afa_grant_use, afa_grant_revoke, afa_grants_list | Delegation, Walkthrough | no |
| Machine delegation grants and the fulfil gate | POST /v1/delegate/grant, GET /v1/delegate/grants, GET /v1/delegate/grant/{grant_id}, POST /v1/delegate/grant/{grant_id}/revoke, POST /v1/delegate/{grant_id}/fulfill | afa_delegation_grant, afa_delegation_get, afa_delegation_list, afa_delegation_revoke | Delegation | no |
| Order-bound delegation tokens, offline verification | POST /v1/delegations, POST /v1/delegations/verify, GET /v1/delegations/service-pubkey, GET /v1/delegations/service-pubkeys | afa_service_pubkey | no | no |
| Hosted record verification | POST /v1/verify/envelope, POST /v1/verify/checkpoint, POST /v1/verify/scientific-campaign | afa_verify_envelope, afa_verify_checkpoint, afa_verify_scientific_campaign | no | no |
| Notifications and webhooks | GET /v1/notifications, GET /v1/webhooks, PUT /v1/webhooks, DELETE /v1/webhooks | afa_notifications_list, afa_webhook_get, afa_webhook_set, afa_webhook_delete | Notices | no |
| Usage and keys | GET /v1/usage/summary, GET /v1/usage/export, GET /v1/me/api-keys | afa_usage | Access | no |
| Free period and continued access | GET /v1/billing/status, POST /v1/billing/transition-request | no | Access | no |
| Seats, and writing to us with no account | GET /v1/seats, POST /v1/inquiries | no | no | no |
| Two pure envelope tools | POST /v1/mcp/call | aa_intent_delta, aa_interference_scan | no | also local |
| Read this skill | GET /v1/skill | afa_skill | Start | no |
| Your own ledger | reconcile reads POST /v1/events/search | no | no | no |

Four statements about this table.

- The hosted MCP tool column describes the tool set the standard MCP endpoint
  serves in this release (section 10). Today the older hosted tool endpoint
  also serves the two pure envelope tools over `POST /v1/mcp/call`.
- Nothing is handed over. There is no client library to install, no archive to
  fetch and no local transport to run. Every surface above is the hosted API,
  the MCP endpoint over HTTP, or the console. Where this document describes a
  file you keep, such as the ledger in section 6, it prints the shape in full
  for you to build rather than shipping one.
- The local operator tools (a command-line tool and a local tool server that
  read and write files on the operator's own machine) exist only inside our
  own environment. They are not a customer surface, they are not shipped, and
  they are not offered as a free or open-source product. Their names are not
  listed here because none of them is reachable from anything you hold.
- Nothing customer-facing runs offline.

## 3. Connect in five minutes

### 3.1 Get a key

Sign in at `https://app.afaprotocol.com` with an email one-time code, open
Access, and issue a key for one machine. The same three calls over REST:

```text
POST /v1/auth/request-otp     {"email": "you@example.com"}
POST /v1/auth/verify-otp      {"email": "you@example.com", "code": "123456"}   -> session cookie
POST /v1/me/api-keys          {"machine": "laptop-1"}
```

The response shows the token once:

```json
{"token": "afa-beta-k3ryn7pq2wsdz4mvbt6xhc5jfa-e4qs",
 "key_prefix": "afa-beta-k3ryn7pq", "format": "afa-beta-v1",
 "warning": "Save this token now. It is never displayed again.",
 "key": {"id": "...", "machine": "laptop-1", "expires_at": "...", "days_until_expiry": 30}}
```

Keys expire after 30 days. Rotate with `POST /v1/me/api-keys/{key_id}/rotate`;
revoke with `DELETE /v1/me/api-keys/{key_id}`. One key per machine. A key
cannot mint keys (403 `api_key_cannot_issue_keys`); only a signed-in session
can.

The token has a shape you can check before you send it:

```text
afa-beta-<26 characters>-<4-character check>
```

The 26 characters and the check both draw from the lower-case base32
alphabet (`a` to `z`, `2` to `7`). The check is the first four characters of
the base32 encoding of the CRC-32 of the 26, lower-cased. In Python:

```python
import base64, struct, zlib

def check_of(body: str) -> str:
    digest = struct.pack('>I', zlib.crc32(body.encode()) & 0xFFFFFFFF)
    return base64.b32encode(digest).decode()[:4].lower()

body, check = token.removeprefix('afa-beta-').rsplit('-', 1)
assert check_of(body) == check
```

This catches a token that lost a character in a shell, a spreadsheet or a
chat window. It proves nothing about whether the token is live or yours:
anyone can compute the check. A token that fails it is refused at the door
with 401 `malformed_key`, before anything is looked up, so you can tell a
mistyped key from a revoked one. Say malformed, never forged.

Keys issued before this format began with `ek-aa-` and carry no check. They
keep working until they expire. Accept both when you validate; only the
branded prefix has a check to test.

### 3.2 First event

```text
curl -s -X POST https://api.afaprotocol.com/v1/events \
  -H "X-API-Key: afa-beta-k3ryn7pq2wsdz4mvbt6xhc5jfa-e4qs" -H "Content-Type: application/json" \
  -d '{"event_id": "e-0001", "event_type": "session_open", "sequence": 0}'
```

```json
{"status": "appended",
 "event": {"event_id": "e-0001", "event_type": "session_open", "sequence": 0,
           "payload_hash": "sha256:...", "ts": "2026-08-31T09:30:02+00:00"},
 "quota": {...},
 "epoch": {"epoch_index": 0, "quadrant": "east", "date_utc": "2026-08-31", "scheme": "..."}}
```

The second event carries `"prev_hash"` equal to the first event's
`payload_hash` and `"sequence": 1`. A wrong `prev_hash` is refused with
409 `prev_hash_mismatch` and the response names `expected` and
`last_sequence`: adopt them, retry once, and if it fails again stop and read
the record. Do not loop.

### 3.3 First verify

```text
GET  /v1/events/e-0001          -> {"event": {...}, "ancestry": []}
POST /v1/chain/checkpoint  {}   -> {"status": "checkpointed",
                                    "block": {"block_index": 0, "range_start": 0, "range_end": 1,
                                              "event_count": 2, "merkle_root": "sha256:...",
                                              "prev_block_hash": null, "block_hash": "sha256:..."}}
POST /v1/chain/verify           -> {"valid": true, "block_count": 1, "broken": []}
```

Read `block_count`. `valid: true` with `block_count: 0` means no block was
checked. It does not mean the record is intact.

### 3.4 Connect a tool host over MCP

There is one transport and it speaks HTTP. The endpoint is
`POST https://api.afaprotocol.com/v1/mcp` with `X-API-Key: afa-beta-...` or
`Authorization: Bearer afa-beta-...`. It ships in this release (section 10).
Until it answers, the older hosted tool endpoint is `GET /v1/mcp/tools` plus
`POST /v1/mcp/call`. Nothing is installed on your machine, so there is no
local process to run and no file to fetch.

A host that takes a command line:

```text
claude mcp add --transport http afa https://api.afaprotocol.com/v1/mcp --header "X-API-Key: afa-beta-k3ryn7pq2wsdz4mvbt6xhc5jfa-e4qs"
```

A host that takes a JSON server list:

```json
{"mcpServers": {"afa": {"url": "https://api.afaprotocol.com/v1/mcp",
                        "headers": {"X-API-Key": "afa-beta-..."}}}}
```

Any host that speaks the streamable HTTP transport can use it. A host that
cannot reach the endpoint has no tools; there is no offline path.

What a key needs: the MCP endpoint accepts any API key of the account for
`initialize` and `tools/list`. Each tool needs the capability of the REST
route it wraps; a key without it receives `scope_missing` naming that
capability. Check: a key scoped to `events:read` lists every tool and calls
afa_events_list, and the same key calling afa_record_event is refused naming
`events:write`.

Keep the key in the host's config file or the operating system keyring. Never
put it in a prompt, a payload, or a committed file.

## 4. Capability reference

For each capability: the route or tool, the minimal request, the minimal
response, and the check that catches it if the claim were false.

### 4.1 Health

`GET /v1/health` (tool: afa_health).

```json
{"status": "ok", "service": "afaprotocol", "version": "0.1.0", "storage": "postgres"}
```

Check: `storage` must read `postgres`. `memory` names a development instance
that accepts writes and loses them on restart.

### 4.2 Record an event

`POST /v1/events` (tool: afa_record_event). Fields: `event_id`, `event_type`,
`sequence` (required); `prev_hash`, `payload_hash`, `tool`, `decision`,
`parent_id`, `ts`, `raw`, `machine_id` (optional). Response: section 3.2.

Limits: 100 events per minute per key (429 with `Retry-After: 60`); 10,000
events per day (429 with `Retry-After: 3600`). `POST /v1/events/batch`
accepts up to 500 events per call (413 above that); an event missing a
required field is skipped and named in `errors` while the rest lands; a
re-sent `event_id` is a no-op.

If the sending machine has a registered signing key (4.12), an event whose
`raw.event_signature` does not verify is refused with 401
`event_signature_invalid`.

Check: the next event's `prev_hash`, and your own ledger's reconcile
(section 6). A record the hosted side altered no longer matches either.

### 4.3 Read back

`GET /v1/events/{event_id}` (afa_event_get) returns the event and up to 50
ancestors by `prev_hash`. `POST /v1/events/search` (afa_events_list) with
`{"event_types": ["tool_call"], "tool": "Read", "limit": 200, "cursor": 412}`
returns newest first; `cursor` means "sequence below this".
`GET /v1/events/tree?root=<event_id>&depth=6` returns the subtree with
`descendant_count`. `GET /v1/events/lineage/{event_id}` (afa_event_lineage)
returns the ordered ancestors and `reached_genesis`. `GET /v1/events/stream`
is a server-sent events tail.

Check: a lineage with `reached_genesis: false` is missing a parent. Do not
present it as complete.

### 4.4 Settlement checkpoint and chain verify

`POST /v1/chain/checkpoint` folds every unsettled event into one block whose
`merkle_root` covers their payload hashes and whose `prev_block_hash` links it
to the previous block. Pass `"signature"` (your Ed25519 over
`merkle_root||prev_block_hash`) and `"signer_id"` to make the block yours.
`GET /v1/chain/blocks` lists blocks. `POST /v1/chain/verify` (afa_chain_verify)
recomputes every `block_hash` and every link:

```json
{"valid": false, "block_count": 3, "broken": [{"block_index": 2, "error": "prev_block_hash_break"}]}
```

Check: a block you signed is one the server cannot rewrite without your key.
An unsigned block proves ordering on the server, not custody.

### 4.5 Scoped cross-session dedup

`POST /v1/dedup/check` (afa_dedup_check):

```json
{"tool": "Read", "tool_input": {"path": "README.md"}, "cache_scope": "intent",
 "parent_id": "env-42", "window_seconds": 21600}
```

Miss: `{"decision": "allow", "dedup_status": "miss", "dedup_action": "execute", ...}`.
Hit: `{"decision": "deny", "dedup_status": "hit", "dedup_action": "reuse",
"cached_from_event_id": "e-0311", "membership_proof": {...},
"scope_proven": true, "anchor": {"status": "client_block_covers", ...}}`.
`"intent": "fresh"` bypasses the check and says so (`dedup_status: bypass`).
A tool not on the cacheable list answers `uncacheable`. `POST /v1/policy/test`
is the same question without a write (`would_hit`).

Check: `scope_proven` is computed against a root the server built from the
same read, so it detects tampering below the API and is not independent of it.
`anchor.status: client_block_covers` means your own signed block (4.4) covers
the cited event; `self_consistent_only` means append a checkpoint before you
rely on the citation. Before reusing a cached result, fetch
`cached_from_event_id` and compare its `payload_hash` with the one you hold.

### 4.6 Sub-agent grants

Section 5 walks one grant through its life. The routes: `POST /v1/grants`
(afa_grant_create), `GET /v1/grants/status?subagent=<name>&tool=<tool>`
(afa_grant_status), `POST /v1/grants/{grant_id}/use` (afa_grant_use),
`POST /v1/grants/{grant_id}/revoke` (afa_grant_revoke), `GET /v1/grants`
(afa_grants_list).

Check: `GET /v1/grants/status` is the reader the gate uses. A grant that is
not visible there does not authorize anything, whatever the create call said.

### 4.7 Machine delegation grants and the fulfil gate

`POST /v1/delegate/grant` (afa_delegation_grant):

```json
{"grantee_machine": "agent-B", "task": "summarize", "scope": "doc-42", "max_uses": 1, "ttl_seconds": 600}
```

201 `{"status": "granted", "grant": {"grant_id": "grant-9f2c...", "grantor_machine": "agent-A",
"expires_at": "..."}, "epoch": {...}}`. The grantor is the machine bound to
your key; a key may not claim another machine (403
`machine_mismatch_for_api_key`).

`POST /v1/delegate/{grant_id}/fulfill` with
`{"task": "summarize", "scope": "doc-42", "result_hash": "sha256:..."}` records
the delegated action only if a live grant authorizes exactly it. Refusals:
403 `grantee_mismatch`, `task_mismatch`, `scope_mismatch`, `grant_expired`,
`grant_revoked`, `grantor_key_unregistered`, `grant_signature_invalid`;
409 `grant_exhausted`. Success: `{"status": "fulfilled", "use_number": 1, "event": {...}}`
where the event has `event_type: delegated_action` and `parent_id` equal to
the grant id.

`GET /v1/delegate/grants` (afa_delegation_list), `GET /v1/delegate/grant/{grant_id}`
(afa_delegation_get), `POST /v1/delegate/grant/{grant_id}/revoke`
(afa_delegation_revoke; 404 `grant_not_found`, 409 `grant_already_revoked`).

Check: the recorded action's lineage (`GET /v1/events/lineage/{event_id}`)
must reach the grant event. An action with no grant in its lineage was not
delegated, whatever its label says.

### 4.8 Order-bound delegation tokens and offline verification

`POST /v1/delegations` with `{"chain": ["orchestrator", "planner", "worker-3"],
"payload": {"task": "ingest", "scope": "batch-7"}}` returns a token:

```json
{"token": {"chain": ["orchestrator", "planner", "worker-3"], "chain_fingerprint": "...",
           "chain_pubkey": "<base64url raw key>", "binding": "ed25519:...",
           "payload": {"task": "ingest", "scope": "batch-7"}, "signature": "ed25519:...",
           "alg": "ed25519", "scheme": "..."}}
```

`POST /v1/delegations/verify` with `{"token": {...}}` answers
`{"valid": true, "reason": "ok"}` or `{"valid": false, "reason": "chain_order_mismatch"}`
(also `payload_tampered`, `malformed`). `GET /v1/delegations/service-pubkey`
(afa_service_pubkey) returns `{"service_pubkey": "<base64url raw 32 bytes>", "alg": "ed25519"}`,
and `GET /v1/delegations/service-pubkeys` returns every key we have published
with its validity window, so a token signed before a rotation still checks.

**Both of those need no key and no account.** Hand a token to somebody who has
never heard of us and they can fetch the key that verifies it. Behind our own
authentication, an offline check would be a round trip through us with a
different name on it.

Offline, with nothing from us: (1) the service key verifies `binding` over the
canonical JSON (sorted keys, no whitespace) of `{"chain", "chain_pubkey",
"scheme"}` as presented; (2) `chain_pubkey` verifies `signature` over the
canonical JSON of `payload`. Reordering the chain changes message (1), so it
fails. Pin the service key at the start of an evaluation; a changed key is a
changed service identity.

Check: run (1) and (2) yourself and compare with the hosted answer. If they
disagree, the token or the key changed between the two reads.

### 4.9 Hosted record verification

`POST /v1/verify/envelope` (afa_verify_envelope) with
`{"envelope": {...}, "public_key_pem": "-----BEGIN PUBLIC KEY-----..."}` and
`POST /v1/verify/checkpoint` (afa_verify_checkpoint) with
`{"checkpoint": {...}, "responder_pubkey": "<base64url raw key>",
"expect_envelope_id": "env-42", "expect_operator_index": 0}`. Both are
stateless: nothing is stored or read from the ledger. Answers:
`{"valid": true, "reason": "ok"}` or a named reason (`unsigned`, `malformed`,
`bad_public_key`, `signature_invalid`). The envelope recipe: the signature
covers the canonical JSON of the record without `signature` and `signer_id`,
with `scope` sorted.

Check: the same arithmetic runs offline. A hosted `valid: true` you cannot
reproduce locally is a finding about the record, not a pass.

`POST /v1/verify/scientific-campaign` (afa_verify_scientific_campaign) accepts
a signed scientific-campaign record chain, its public key, and its campaign_id.
It verifies record hashes, signatures, contiguous sequence numbers, parent
links, signer consistency, and artifact-digest syntax, then reports the latest
stage and claim state. It is stateless and does not receive raw research
payloads. A valid answer proves integrity and ordering of the submitted
records; it does not prove the scientific conclusion, verify remote artifact
bytes, or establish trust in a caller-supplied signing key. Pin the signer
fingerprint separately and verify the referenced artifacts locally.

### 4.10 Notifications and webhooks

`GET /v1/notifications` (afa_notifications_list). `PUT /v1/webhooks`
(afa_webhook_set) with `{"url": "https://example.com/afa"}`; a non-http(s)
URL is refused with 422 rather than stored. `GET /v1/webhooks`
(afa_webhook_get) answers `{"configured": true, "url": "..."}`;
`DELETE /v1/webhooks` (afa_webhook_delete).

Delivery is a queued POST of `{"body": "...", "severity": "warning", "type": "grant.revoked"}`
(sorted keys) carrying an `X-AFA-Signature: sha256=<hmac hex>` header computed
with a per-account secret. Limit, stated plainly: that secret is not yet issued
to you, so today the header is present but you cannot check it. Treat a
webhook as a prompt to re-read `GET /v1/grants/status`, which is the
authority, not as proof by itself. Delivery is best effort; a failed delivery
never makes a successful revoke look failed.

### 4.11 Usage

`GET /v1/usage/summary?month=2026-08` (afa_usage) returns monthly totals per
capability and per key, with the account's free-period block:
`billing_status`, `free_period_ends_at`, `days_remaining` and `billed`, which
is false. `GET /v1/usage/export?from=2026-08-01&to=2026-08-31&format=csv`
returns the rows.

**No price is served here or anywhere else.** There is no estimate field, no
currency and no rate. What happens after the free period is arranged with us
(section 12), and a number returned by an API would be a quote nobody agreed
to. What you get is the count, which is the fact the conversation is had
against.

Check: the export rows sum to the summary. If they do not, keep both and ask.

### 4.12 Identity and signed events

`POST /v1/identity/register` with `{"machine_id": "laptop-1", "public_key_pem": "..."}`
returns `{"status": "registered", "fingerprint": "sha256:<32 hex>"}`; a key
that is not Ed25519 is refused (400). From then on, events from that machine
must carry a signature the key verifies.

The recipe the hosted API verifies: build the event object without the four
fields `event_signature`, `event_signature_alg`, `event_signature_key`,
`event_signature_status`; serialise it as JSON with sorted keys (Python
`json.dumps(obj, sort_keys=True)`, UTF-8); sign the bytes with your Ed25519
private key; store `event_signature` as `"ed25519:" + base64url(signature)`
with padding stripped, `event_signature_alg` as `ed25519-sha256-json-v1`,
`event_signature_key` as your key fingerprint, `event_signature_status` as
`signed`. Send the signed object as `raw` in `POST /v1/events`. The server
holds no signing key for you: it can check your record, it cannot write it.

Check: `GET /v1/identity` lists the registered keys and fingerprints. An event
accepted from a machine with no registered key is unverified, and the response
says so.

### 4.13 The hosted tool endpoint today

`GET /v1/mcp/tools` lists the exposed tools; `POST /v1/mcp/call` with
`{"tool": "aa_intent_delta", "arguments": {"before": {...}, "after": {...}}}`
runs one. Two pure tools are served: aa_intent_delta (a structured diff
between two authority envelopes: scope added and removed, actor and action
changes) and aa_interference_scan (concurrent exclusive-scope conflicts across
a set of envelopes within a time window). Both compute from the request body
alone. Any other name gets the same 404 `unknown_tool`, whether or not the
name means anything elsewhere.

### 4.14 Hosted tool names

The fixed contract, one name per line:

```text
afa_health
afa_surfaces
afa_skill
afa_record_event
afa_events_list
afa_event_get
afa_event_lineage
afa_chain_verify
afa_dedup_check
afa_grant_create
afa_grant_status
afa_grant_use
afa_grant_revoke
afa_grants_list
afa_delegation_grant
afa_delegation_get
afa_delegation_list
afa_delegation_revoke
afa_service_pubkey
afa_verify_envelope
afa_verify_checkpoint
afa_verify_scientific_campaign
afa_notifications_list
afa_webhook_get
afa_webhook_set
afa_webhook_delete
afa_usage
aa_intent_delta
aa_interference_scan
afa_governance_propose
afa_governance_ratify
afa_governance_enact
afa_governance_list
```

### 4.16 Your free period, and asking to continue

`GET /v1/billing/status` returns where this account stands:

```json
{"free_period_ends_at": "2026-10-07T09:14:02+00:00",
 "billing_status": "free_period",
 "days_remaining": 12,
 "free_period_days": 30,
 "transition_request": null,
 "request_route": "POST /v1/billing/transition-request"}
```

`billing_status` is one of `not_started` (no key has been created, so no clock
has started), `free_period`, `free_period_ended`, or `free_period_unknown`
(the date could not be read, which is not the same as either answer).

Every response also carries `X-AFA-Billing` with that status, and
`X-AFA-Free-Period-Ends` while the period is running, so you learn the date
from a call you were making anyway.

To ask for continued access, `POST /v1/billing/transition-request`:

```json
{"organisation": "Example Ltd",
 "use_case": "attestation for a nightly agent run",
 "expected_monthly_calls": 50000,
 "contact_email": "ops@example.com",
 "notes": "happy to talk any weekday"}
```

One request per account; sending it again updates the one you have and keeps
the date you first asked. `contact_email` and `notes` are optional and the
contact defaults to the account address. In the console the same thing is the
Request continued access button.

Check: `GET /v1/billing/status` afterwards carries your request under
`transition_request` with `submitted_at` and a `status`.

### 4.17 Seats

`GET /v1/seats` needs no key:

```json
{"capacity": 40, "used": 11, "remaining": 29,
 "note": "Seats are bounded by review capacity, not by infrastructure."}
```

When `remaining` is zero a NEW address asking for a sign-in code is answered
`423 seats_full` and pointed at the route below. An account that already
exists is unaffected, whatever the number says.

### 4.18 Writing to us without an account

`POST /v1/inquiries` takes `{"name", "email", "organisation", "message",
"source"}` where `source` is `site` or `console`, and answers `202
{"status": "received"}`. No key, no account. Five an hour per address; over
that it answers 429 and names the address to write to instead.

### 4.15 This document over the API

`GET https://api.afaprotocol.com/v1/skill` returns this text as
`text/markdown`. No key is required, because an agent that cannot read its
instructions until it holds a key cannot follow them. The same bytes are at
`https://app.afaprotocol.com/docs/skill.md` and on the Start tab of the
console.

Send a key with the request and the copy you get is addressed to your
organisation; nothing else in it changes. The `afa_skill` tool returns the
same text in one field.

Check: fetch it without a key and with one, and diff the two. Only the title
line and the first heading differ.

## 5. Grants, budgets, containment, revocation push, offline order verification

One grant, from issue to revocation, with the record each step leaves.

09:30:00Z. The orchestrator issues a grant for `reader-1`:

```text
POST /v1/grants
{"grant_id": "grant-8c1f", "subagent": "reader-1", "parent_agent": "orchestrator",
 "allowed_tools": ["Read", "Grep"], "max_tool_calls": 5,
 "expires_at": "2026-08-31T12:00:00+00:00"}

201 {"grant": {...}, "status": {"authorized": true, "status": "authorized", "subagent": "reader-1", ...}}
```

The response carries the status read back through the same reader the gate
uses. `expires_at` is absolute (a duration is refused), `max_tool_calls` is a
positive integer, `subagent` is the exact name the orchestrator will delegate
to. A payload using an alias for any of these is refused with 422
`malformed_grant`, and the reason names the correct key. Nothing is written.

09:30:04Z. Before delegating, the orchestrator asks the gate:

```text
GET /v1/grants/status?subagent=reader-1&tool=Read
{"authorized": true, "status": "authorized", "subagent": "reader-1", "reason": "live grant grant-8c1f authorizes ..."}
```

10:02:10Z. Twelve workers race the budget. Each records a use before it acts:

```text
POST /v1/grants/grant-8c1f/use   {"subagent": "reader-1", "n": 1}
200 {"outcome": "consumed", "grant_id": "grant-8c1f", "uses": 3, "remaining": 2}
409 {"error": "budget_exhausted", "outcome": "budget_exhausted", "uses": 5, "remaining": 0,
     "reason": "grant 'grant-8c1f' has 0 of 5 calls remaining; refusing to record 1 ..."}
```

Exactly five landed and seven were refused, each refusal with its reason. A
use that would cross the budget is refused whole, never partly recorded. From
then on the gate answers `budget_exhausted`. Metering counts what an
integration reports: it bounds an honest integration, and an integration that
never reports is bounded only by expiry and revocation.

10:05:30Z. `reader-1` tries to delegate onward with more than it holds:

```text
POST /v1/grants
{"grant_id": "grant-9d0a", "subagent": "reader-1-child", "parent_agent": "reader-1",
 "allowed_tools": ["Read", "Grep", "Bash"], "max_tool_calls": 50,
 "expires_at": "2026-08-31T18:00:00+00:00"}

422 {"error": "malformed_grant",
     "reason": "grant 'grant-9d0a' for sub-agent 'reader-1-child' would exceed the authority of its
                parent 'reader-1': tools not held by parent 'reader-1': ['Bash']; expiry later than
                the parent's; max_tool_calls larger than the parent's. A delegation cannot grant more
                than the delegator holds. ..."}
```

Containment is checked against the parent's live grant on all three axes:
tools, expiry, budget. A parent with no grant of its own imposes no
containment and is bounded instead by the operator-configured root ceiling.

10:14:00Z. The orchestrator revokes:

```text
POST /v1/grants/grant-8c1f/revoke   {"subagent": "reader-1"}
200 {"outcome": "revoked", "grant_id": "grant-8c1f",
     "status_after": {"authorized": false, "status": "revoked", ...}}
```

`status_after` is re-read through the reader that authorizes; it is the only
success signal that counts. A revoke that matches nothing answers 404
`no_grant`, never a bare success. If a webhook is registered, a
`grant.revoked` notification is queued as well (4.10). Revocation is
poll-effective at the gate; the push narrows the window in which an
orchestrator keeps delegating on stale authority.

Offline order verification, for the delegation token that carried this work
across machines: fetch the service key once (`GET /v1/delegations/service-pubkey`),
pin it, and run the two Ed25519 checks in 4.8 on every token you receive. A
token whose chain was reordered fails check (1) with `chain_order_mismatch`,
on your machine, with no call to us.

## 6. Keep your own ledger

The hosted record is one reader of what your agents did. Keep a second one
you control, then compare the two. When they disagree, rely on neither until
you know which side moved.

Nothing ships for this. The store shape is printed in full below and on the
local ledger page of the route reference, so you can read it, copy it and run
it against a SQLite file you own. It needs no library of ours: the DDL is
plain SQLite, and the two verifiers and the reconcile step in 6.2 and 6.3 are
described as procedures you implement in whatever language your agent already
runs in.

### 6.1 The store

```sql
-- AFA Protocol: reference local ledger (SQLite).
--
-- This is the store shape we recommend for the ledger you keep on your own
-- machine. It is the same shape our own local ledger converged on after its
-- chain-break count was measured at 7.98% and driven to zero. Every rule
-- below exists because its absence produced a defect we recorded.
--
-- Rules, in plain language:
--
--   1. WAL mode. Readers never block the single writer and a crash mid-write
--      leaves the previous state intact.
--   2. Every append runs under BEGIN IMMEDIATE. The appender takes the write
--      lock BEFORE it reads the chain tip, so two concurrent appenders cannot
--      both read the same tip and both link to it. A bare read-then-write of
--      the tip forks the chain under concurrency; the lock is what made our
--      own fork count zero.
--   3. `seq` is insertion order and belongs to the store. It is separate from
--      any sequence number a client puts on its own events (`client_sequence`).
--      Two writers can each believe they hold "sequence 7"; only one of them
--      is row 7.
--   4. The payload is NOT stored. `payload_hash` is a SHA-256 over the
--      canonical JSON of the payload. Anyone who can read this file learns
--      that something happened, when, and by whom, not what was said.
--   5. `prev_hash` is the chain hash of the previous row in `seq` order, and
--      `chain_hash` is a SHA-256 over the canonical JSON of this row's chained
--      fields INCLUDING `prev_hash`. Altering an old row changes its chain
--      hash, and the next row's `prev_hash` no longer matches it.
--   6. `chain_state` is a single row holding the tip. It is written in the
--      SAME transaction as the insert, so the tip can never point at a row
--      that does not exist and a row can never exist that the tip missed.
--   7. `malformed_lines` is quarantine. A reader that swallows a parse error
--      and moves on is more broken than the data it skipped. Anything that
--      cannot be parsed is stored here with the reason, never discarded.
--   8. `anchors` holds hosted checkpoint receipts: the hosted record's root
--      and event id at the moment you anchored, so a later reconcile can say
--      which side moved.
--
-- Two verifiers read this file, and both are required:
--   verify_signatures: were the bytes of a row altered after it was signed?
--   verify_chain:      is every row still in its original position?
-- A green signature check never means an intact chain. Reordered rows carry
-- valid signatures. Run both.

PRAGMA journal_mode = WAL;

CREATE TABLE IF NOT EXISTS store_meta (
    key   TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS events (
    seq                INTEGER PRIMARY KEY AUTOINCREMENT,
    event_id           TEXT    NOT NULL UNIQUE,
    event_type         TEXT    NOT NULL,
    tool               TEXT,
    decision           TEXT,
    parent_id          TEXT,
    client_sequence    INTEGER,
    prev_hash          TEXT    NOT NULL,
    payload_hash       TEXT    NOT NULL,
    chain_hash         TEXT    NOT NULL UNIQUE,
    signature          TEXT,
    signer_fingerprint TEXT,
    surface            TEXT    NOT NULL
        CHECK (surface IN ('rest', 'mcp', 'mcp_stdio', 'sdk', 'cli', 'hook', 'dashboard')),
    created_at         TEXT    NOT NULL
);

CREATE INDEX IF NOT EXISTS events_by_type    ON events (event_type);
CREATE INDEX IF NOT EXISTS events_by_created ON events (created_at);

CREATE TABLE IF NOT EXISTS chain_state (
    id         INTEGER PRIMARY KEY CHECK (id = 1),
    tip_hash   TEXT    NOT NULL,
    max_seq    INTEGER NOT NULL,
    updated_at TEXT    NOT NULL
);

CREATE TABLE IF NOT EXISTS malformed_lines (
    id             INTEGER PRIMARY KEY AUTOINCREMENT,
    raw_line       TEXT NOT NULL,
    reason         TEXT NOT NULL,
    quarantined_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS anchors (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    checkpoint_id   TEXT NOT NULL UNIQUE,
    root            TEXT NOT NULL,
    hosted_event_id TEXT,
    received_at     TEXT NOT NULL
);
```

Hash recipes, so a third party can recompute every value with nothing from us.
Canonical JSON means sorted keys, no whitespace, UTF-8.

- `payload_hash` = `"sha256:"` + hex SHA-256 of the canonical JSON of the payload.
- `chain_hash` = `"sha256:"` + hex SHA-256 of the canonical JSON of
  `{event_id, event_type, tool, decision, parent_id, client_sequence, payload_hash, surface, created_at, prev_hash}`.
- The first row's `prev_hash` is `"sha256:"` followed by 64 zeros.
- `signature` = `"ed25519:"` + base64url (padding stripped) of an Ed25519
  signature over the UTF-8 bytes of the `chain_hash` string.
- `signer_fingerprint` = `"sha256:"` + the first 32 hex characters of SHA-256
  over the public key PEM bytes, the same recipe the hosted identity registry
  uses.

### 6.2 Writing and verifying

Three operations to implement over the table above, with the answers each one
owes you. Names are ours for reference; yours can differ. The shapes below are
what a caller has to be able to read, whatever language you write it in.

```text
append(event)          -> row with chain_hash and seq
                          hashes the payload, stores the hash, never the payload
                          runs inside BEGIN IMMEDIATE so two writers cannot fork

verify_chain()         -> {"status": "ok", "checked": 1, "breaks": [],
                           "tip_hash": "sha256:...", "state_matches": true}

verify_signatures(keys)-> {"status": "ok", "checked": 1, "valid": 1,
                           "invalid": 0, "unsigned": 0}
```

Every result carries a `status`. `verify_signatures` answers `ok`,
`partial` (unsigned rows or a signer you did not supply a key for), `invalid`,
`empty`, or `not_measured` when no signature library is available. It must
never answer `ok` for a check it did not run. `verify_chain` answers `ok`,
`broken` (each break named with its `seq` and kind: `prev_hash_mismatch`,
`chain_hash_mismatch`, `genesis_mismatch`, `tip_state_mismatch`), or `empty`.
A tip read answers `ok`, `empty`, or `inconsistent` when the state row
disagrees with the rows, which a correct appender can never produce.

Both verifiers, every time. A tampered field turns both red. Two rows swapped
in place turn only the chain check red while every signature still verifies.
A forged signature turns only the signature check red while the chain is
intact. Each check is blind to what the other sees.

Input you cannot parse goes to quarantine with the reason, never to the
floor: an ingest of a good line appends it, and a bad one answers
`{"status": "quarantined", "reason": "json_parse_error: ..."}` and keeps the
raw bytes in the quarantine table for someone to read later.

### 6.3 Reconcile: two readers, and what to do when they disagree

Page `POST /v1/events/search` for the same machine, then compare it with your
own rows. The report a reconcile owes you:

```text
reconcile(hosted_events) ->
  {"status": "agree" | "disagree", "local_count": 412, "hosted_count": 412,
   "common": 412, "missing_on_hosted": [], "extra_on_hosted": [],
   "hash_mismatch": [], "misordered": [], "note": "..."}
```

`missing_on_hosted` names local events the hosted record lacks;
`extra_on_hosted` the reverse; `hash_mismatch` events present on both with a
different `payload_hash`; `misordered` events the hosted side placed earlier
than an event that precedes them locally. Any non-empty list is `disagree`.

For the two readers to compare like with like, send the hosted API the same
`event_id` and the same `payload_hash` you wrote locally (supply
`payload_hash` explicitly in `POST /v1/events`; otherwise the server computes
its own hash over the whole event and the two sides differ by construction,
which is a configuration fact, not tampering).

When the report says `disagree`: stop treating either copy as the record,
keep both, find which side moved (your quarantine table and the hosted
`ancestry` are the first two places to look), and only then decide. Two
independent readers that disagree are a finding, not a nuisance.

### 6.4 One ledger per piece of work

Keep separate work in separate chains rather than one chain for everything.
The hosted record already does this: every event carries a `machine_id`, and
the service keeps a chain per `(account, machine_id)` with its own head, its
own sequence and its own hash. `GET /v1/dashboard/machines` lists them, each
with its head and when it last moved. An event sent with an API key inherits
that key's machine when it names none.

So the split is free, and the choice is what you put in the field. A
hostname is the obvious value and it is rarely the useful one. One value per
piece of work is better: one deployment pipeline, one customer's data, one
long-running agent. Then a break in one chain is a break in one piece of
work, a verify of one chain answers about one piece of work, and an export
for a reviewer carries what they asked about and nothing else.

Do the same in your own ledger. The reference store in 6.1 keeps `machine`
on every row for this reason: verify per machine, reconcile per machine, and
compare like with like against the hosted side.

Two things it does not buy you. It is not an access boundary: one key can
write to any machine name, and the names live in one account. And it is not
retroactive, since events already written keep the machine they were written
with.

Check: `GET /v1/dashboard/machines` shows one row per name you have used,
each with its own head. If everything you have written sits under one row,
you have one ledger and the paragraphs above are what you are missing.

## 7. Best practices from the runs

Eleven lessons from our own multi-agent runs against the live API. Each names
its check.

1. A lock coordinates, a grant accounts. Twelve agents raced one grant with a
   budget of five: exactly five uses landed and seven were refused, each
   refusal recorded with `budget_exhausted` and the count remaining. A mutex
   would have serialised them and left no record of who was refused. Check:
   the gate answers `budget_exhausted` once recorded uses reach the budget.
2. Dedup pays across sessions and machines, not inside one session.
   Byte-identical repeats inside a single session were under three percent of
   calls. Scope the check to a parent or an intent and let it span sessions.
   Check: `cached_from_event_id` on a hit points at an earlier session's event.
3. Delegation is order-sensitive. A child grant may not exceed its parent on
   tools, expiry or budget, and overreach is refused at every depth with 422
   `malformed_grant` naming the excess. Check: the refusal text lists the
   tools the parent does not hold.
4. A grant is authorization, not decoration. An absent grant reads
   `no_grant`; a malformed grant fails closed and writes nothing; a revoke
   that reports success is visible as `revoked` through the same reader that
   authorizes. Check: `status_after` on the revoke response.
5. On a deny: stop, escalate to a person, or narrow scope. Never retry through
   another tool, never build a path around the rule, never edit the rule that
   fired. An escalation changes who authorized the action; it never changes
   the mechanism. Check: the deny event and the escalation event sit in the
   same lineage.
6. Always run both verifiers. A green signature check on reordered rows is
   the exact case each verifier is blind to alone. Check: section 6.2.
7. Chain breaks are recorded as bridge events, not smoothed over. A break you
   bridged is part of the history; a break you hid is a second defect. Check:
   `GET /v1/events/lineage/{event_id}` reaches genesis across the bridge.
8. Stress the four axes before relying on a setup: concurrency (many writers
   on one grant), the expiry boundary (a grant read one second before and one
   second after `expires_at`), cross-scope replay (a cached result from one
   intent presented under another), cross-user isolation (a key reading
   another account's grant). Check: each axis has a refusal you can show.
9. The constrained party never holds the pen. Policy changes go propose
   (the agent), ratify (a different identity, a person), enact (the system).
   An agent that can enact its own rule is not constrained by it. Check: the
   ratify record carries a different signer than the proposal.
10. Budget-constrained work: given a fixed number of checkpoints, place them on
    observed drift, not on a fixed timer. Check: checkpoints cluster where the
    record shows change, not at round intervals.
11. Secrets never enter a payload: hashes only. The key that signs must not sit
    unencrypted beside the record it signs, or anyone who can read that disk
    can write an entry in your name and the record will not show it. Check:
    search your ledger file for any key material; the only key-shaped string
    in it should be a fingerprint.

## 8. General policies

- Least scope: a grant names the tools it needs and nothing more.
- Explicit expiry on every grant, as an absolute UTC timestamp.
- A named owner per task, recorded as `parent_agent`.
- Budgets on anything that spends: `max_tool_calls` on grants, `max_uses` on
  machine delegations.
- Human checkpoints before irreversible or outward-facing actions, recorded
  as signed checkpoints bound to the step they approve (4.9).
- One signing identity per machine, registered with `POST /v1/identity/register`.
- Key rotation: keys live 30 days; rotate before `days_until_expiry` reaches
  seven.
- Verify offline with the published service key (4.8). Do not take a hosted
  `valid: true` on faith when you can recompute it.
- Keep your own ledger and reconcile (section 6).

### 8.1 A starter grant you can post unchanged

Every rule above is a sentence until something enforces it. This is those
sentences as one body. Change the four names, keep the shape, and post it to
`POST /v1/grants`:

```json
{
  "grant_id": "starter-2026-10-01",
  "subagent": "worker-1",
  "parent_agent": "orchestrator",
  "allowed_tools": ["Read", "Grep"],
  "max_tool_calls": 50,
  "expires_at": "2026-10-01T00:00:00+00:00"
}
```

What each field is doing, and which of the rules above it enforces:

- `allowed_tools` is least scope. Two entries, not a wildcard. A tool absent
  from this list is refused at the gate, not warned about.
- `expires_at` is an explicit absolute UTC expiry. There is no default and no
  open-ended grant; a rolling window is a grant that never ends.
- `parent_agent` is the named owner. It is also what containment compares
  against: this grant cannot exceed the parent's own standing authority.
- `max_tool_calls` is the budget. At 50 recorded uses the gate answers
  `budget_exhausted` and stops, rather than logging an overrun.

Human checkpoints are the one item in section 8 that is not a grant field.
Record one as an event before the step it approves, with a `parent_id` naming
that step, and check for it before you act (4.2 and 4.3). A checkpoint that is
written after the action it approves is a note, not an approval.

Check: `GET /v1/grants/status?subagent=worker-1` immediately afterwards. It
must say `authorized`. If it does not, the grant you wrote is not the grant
the gate can see, and the reason is in the response.

## 9. What you must not do

- Do not act on a sub-agent's behalf without reading `GET /v1/grants/status`
  first. A grant you wrote is not a grant the gate can see until that reader
  says so.
- Do not store payloads, prompts or tool output in the ledger. Hashes only.
- Do not fabricate `prev_hash`, signatures or `parent_id` values, and do not
  retry a 409 `prev_hash_mismatch` more than once without reading the record.
- Do not continue after a deny without a recorded escalation by a different
  identity.
- Do not widen your own scope. Ask the delegator for a new grant.
- Do not treat a webhook as proof of anything. Re-read the gate.
- Do not read `valid: true` from `POST /v1/chain/verify` as an intact record
  when `block_count` is 0.
- Do not put an API key in a prompt, a payload, a URL or a committed file.
- Do not present a lineage that did not reach genesis as complete.
- Do not describe the local operator tools as a customer product, a free plan
  or open source. They are not.
- Do not record an action that did not happen, an approval nobody gave, or a
  check that did not run. That is the one use of this service that makes every
  other record in it worth less, and it is a breach of the acceptable use
  policy you accepted at sign-in.
- Do not use this record where it would be relied on for a decision affecting
  somebody's safety. It has not been assessed for that and none of it is
  designed to fail safe in the sense that field means.

## 10. Arriving in this release

The following are being built now and may already answer on the hosted API;
the route reference at `https://app.afaprotocol.com/docs/` carries the current
state. Nothing else in this document depends on them.

- `POST /v1/mcp`: the standard MCP transport carrying the hosted tool set in
  4.14 (`X-API-Key` or `Authorization: Bearer`). It is the only transport;
  there is no local one.
- `GET /v1/surfaces`: the surface matrix in section 2, served as JSON
  (tool: afa_surfaces).
- `/v1/governance/proposals`, with `/{proposal_id}/ratify`,
  `/{proposal_id}/enact` and `/{proposal_id}/reject`: propose, ratify, enact.
  An enacted deny rule refuses a grant at the writer and at the gate with
  `denied_by_governance` naming the proposal.
- `GET /v1/usage/summary` and `GET /v1/usage/export` (4.11).
- `GET /v1/anchors` and `POST /v1/anchors/run`: receipts for your chain root
  anchored outside the service, each with `how_to_check` instructions.
- `GET /v1/delegations/service-pubkeys`: every service key with its validity
  window, so an evaluator can pin the one that signed a token from before a
  rotation.

## 11. Where to look when something breaks

| You see | It means | Do |
|---|---|---|
| `GET /v1/health` storage `memory` | not the production service | stop; check the host you configured |
| 401 on any call | key missing, expired (30 days) or revoked | issue or rotate a key in Access |
| 401 `event_signature_invalid` | the machine has a registered key and the event's signature does not verify | check the recipe in 4.12; do not resend unsigned |
| 409 `prev_hash_mismatch` | another writer advanced the chain | adopt `expected`, retry once, then read the record |
| 409 `non_monotonic_sequence` | your `sequence` did not increase | resume from `last_sequence + 1` |
| 422 `malformed_grant` | a required grant field is missing, aliased or unparseable | fix the named key; nothing was written |
| 422 `malformed_grant` naming a parent | the child would exceed the parent | narrow the child or widen the parent first |
| 404 `no_grant` on revoke | nothing matched the id and `subagent` | check the exact name; a revoke never succeeds silently |
| 409 `budget_exhausted` | recorded uses reached `max_tool_calls` | stop delegating; issue a new grant deliberately |
| 429 with `Retry-After` | 100 per minute or 10,000 per day per key | wait the stated seconds; batch |
| `reached_genesis: false` | a parent event is missing | do not present the lineage as complete |
| reconcile `disagree` | the two readers differ | keep both; find which side moved before deciding |
| `verify_signatures` `not_measured` | `cryptography` is not installed | install it; a signature check that did not run is not a pass |

## 12. The free period, and what happens after it

**Your account is free for 30 days from the day your FIRST API KEY is
created.** Not from the day you signed up. Signing in to read the documents
does not start the clock; issuing a key does, because a key is the first
moment the account can do anything at all. Before any key exists,
`GET /v1/billing/status` reads `not_started` and the date is null.

**You are told four times.** Ten days before the date, five days before, one
day before, and on the day itself. Each notice goes to the account address
once, in the console and by email.

**What changes on the date: nothing.** Reads and writes both continue.

**What changes seven days after it.** From then on a WRITE is refused with
`402` and a body naming the route that lifts it. A READ is never refused, at
any point, ever, and your record stays readable and exportable including after
a refusal. The refusal covers writes over the API and the same calls made
through the tool transport; a read tool over that transport is unaffected.

```json
{"error": "paid_pilot_required",
 "request": "POST /v1/billing/transition-request",
 "free_period_ended_at": "2026-10-07T09:14:02+00:00"}
```

**Asking is enough to stop it, and any status counts.** Post to
`/v1/billing/transition-request` (4.16), or press Request continued access in
the console. That lifts the refusal at once and keeps it lifted, whatever we
do about the request afterwards and however long we take. We are the slow
party in that conversation.

**No price appears anywhere in this service.** Not in a response, not in a
notice, not in this document. There is no automatic charge and no stored
payment method. What continued access involves is agreed in a conversation,
and `GET /v1/usage/summary` (4.11) shows what your account actually called so
you can read the numbers before we speak.

If you would rather call a client library than the API, ask us. There is
nothing to download, no package to install and no archive to fetch; that is
the same statement section 2 makes about every other surface here.

Write to `idgafholdingsllc@gmail.com`, or `POST /v1/inquiries` (4.18).

Support: `idgafholdingsllc@gmail.com`. Infrastructure Driven Growth And Future
(IDGAF Holdings LLC).
