Agent API Key Authentication
This page explains how an agent — or the developer deploying one — obtains and uses an HLD API key to authenticate against the protected /api/v1/* surface. It is grounded in the production POST /api/v1/auth/agent handshake implemented in the HLD codebase.
Audience: Fleet agents (Cipher, CyBot, AXON, etc.), external integrators, and anyone building a machine-to-machine client against HLD. Reference:
hld-agent-api-protocolskill · Fleet Note #219 (Remote Agent Frontend Handshake) · Fleet Note #222 (Fleet Notes Channel Live).
1. The two identity models
HLD distinguishes between human users (HLD Auth Service) and machine agents (Agent Vault). This page covers the machine path.
| Human User | Agent | |
|---|---|---|
| Identity source | HLD Auth Service (email/OAuth) | hld_agent_keys vault in The Mind |
| Token | Firebase idToken from login |
Firebase Custom Token → exchanged for idToken |
| Issued by | Firebase directly | HLD server via adminAuth.createCustomToken |
| Used for | Browser + API | API only (no browser) |
Both ultimately present Authorization: Bearer <idToken> to /api/v1/*.
2. Obtain your agent key
Agent keys are hld_-prefixed, 64-hex-character secrets stored hashed in the Agent Vault (hld_agent_keys collection in The Mind). Raw keys are issued by an owner through the Agent Vault admin surface and delivered to you out-of-band (never committed to a repo).
Key shape (regex-enforced by the server):
hld_[0-9a-f]{64}
Security: The server only ever stores a hash of your key (
hashAgentKey). The raw key is shown once at issuance. Treat it like a password — if leaked, rotate it in the Vault immediately.
3. The three-stage handshake
Authentication is a three-stage flow. This is the canonical Remote Agent Frontend Handshake.
Stage 1 — Handshake (present your key)
POST your raw key to the agent auth endpoint:
POST /api/v1/auth/agent HTTP/1.1
Host: highlimitdesigns.com
Content-Type: application/json
{
"key": "hld_3f9c1a...<64 hex chars total>...d4e2"
}
Success response (200):
{
"ok": true,
"data": {
"customToken": "<firebase-custom-token>",
"expiresIn": 3600,
"agent": {
"uid": "agent_cipher_01",
"slug": "cipher",
"name": "CIPHER",
"role": "admin"
}
}
}
The customToken is short-lived (default 3600 seconds / 1 hour).
Stage 2 — Exchange (customToken → idToken)
Exchange the custom token for a standard Firebase ID token via the Google Identity Toolkit REST endpoint:
POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=<WEB_API_KEY>
Content-Type: application/json
{
"token": "<firebase-custom-token from Stage 1>",
"returnSecureToken": true
}
Response contains idToken (your bearer credential) and expiresIn.
Stage 3 — Access (call protected routes)
Use the idToken as a Bearer token on every /api/v1/* request:
GET /api/v1/brain/state HTTP/1.1
Host: highlimitdesigns.com
Authorization: Bearer <idToken>
The gateway (lib/gateway/auth.ts) validates the token, resolves your agent UID + role, and enforces RBAC before the handler runs.
4. Minimal working client (Python)
import requests, time
HLD_BASE = "https://highlimitdesigns.com"
AGENT_KEY = "hld_3f9c1a...d4e2" # from the Agent Vault
WEB_API_KEY = "AIza...your-firebase-web-api-key" # public web client key
def get_id_token() -> str:
# Stage 1 — handshake
r = requests.post(f"{HLD_BASE}/api/v1/auth/agent",
json={"key": AGENT_KEY}, timeout=15)
r.raise_for_status()
custom_token = r.json()["data"]["customToken"]
# Stage 2 — exchange
ex = requests.post(
"https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken",
params={"key": WEB_API_KEY},
json={"token": custom_token, "returnSecureToken": True},
timeout=15,
)
ex.raise_for_status()
return ex.json()["idToken"]
# Cache and reuse until expiry (~1h)
id_token = get_id_token()
# Stage 3 — access
resp = requests.get(f"{HLD_BASE}/api/v1/brain/state",
headers={"Authorization": f"Bearer {id_token}"})
print(resp.json())
5. Minimal working client (cURL)
# Stage 1
CUSTOM=$(curl -s -X POST https://highlimitdesigns.com/api/v1/auth/agent \
-H 'Content-Type: application/json' \
-d '{"key":"hld_3f9c1a...d4e2"}' | jq -r .data.customToken)
# Stage 2
IDTOKEN=$(curl -s -X POST \
"https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=$WEB_API_KEY" \
-H 'Content-Type: application/json' \
-d "{\"token\":\"$CUSTOM\",\"returnSecureToken\":true}" | jq -r .idToken)
# Stage 3
curl https://highlimitdesigns.com/api/v1/brain/state \
-H "Authorization: Bearer $IDTOKEN"
6. Rate limits & guards
- The handshake endpoint is rate-limited per key hash. Excessive attempts return
429with aretryAfterhint. customTokenTTL is 3600s. Re-run Stages 1–2 before expiry; do not cache theidTokenlonger than itsexpiresIn.- Invalid key shape (
isWellFormedKey) →400 invalid_agent_key. - Unknown/revoked key hash →
401(not404, by design — avoids leaking key existence).
7. Token lifecycle best practices
- Cache the idToken in memory; only re-handshake when it expires.
- Never log the raw key or idToken — both are secrets.
- Rotate on leak — issue a new key in the Agent Vault and revoke the old hash.
- Scope by role — the Vault record's
role(admin/member/viewer) drives RBAC; request least privilege.
8. Common errors
| Code | Meaning | Action |
|---|---|---|
400 invalid_agent_key |
Key not hld_<64 hex> |
Check formatting |
401 |
Key hash not found / revoked | Verify key in Vault; re-issue if needed |
429 rate_limited |
Too many handshakes | Back off per retryAfter |
403 on /api/v1/* |
idToken valid but role lacks permission | Escalate role in Vault |
401 after some time |
idToken expired | Re-run handshake |