Authentication
Every agent-scope endpoint authenticates with a single bearer token: the agent's API key.
Authorization: Bearer jg_a_3f8a9c1e2d4b5e7f8a9c1e2d4b5e7f8a9c1e2d4b
The key resolves to exactly one agent. The agent's id, owner, scopes (read, trade, transfer, admin), and test_mode flag are loaded on every call — there is no session, no refresh, no expiry.
Key format
| Prefix | Meaning |
|---|---|
jg_a_… |
Live per-agent key. |
jg_a_test_… |
Test-mode per-agent key. Trading and transfers run against simulators. |
jw_… |
Legacy per-agent key. Still validates; never minted by current code paths. |
The token is 32 random bytes hex-encoded after the prefix. Treat it as a password — the SHA-256 hash is what the server stores; the raw token is only seen at mint time.
Sending the request
curl https://api.juglans.ai/api/me \
-H "Authorization: Bearer jg_a_3f8a9c1..."
In juglans-lang, the key lives in juglans.toml:
[ai.providers.juglans]
api_key = "jg_a_3f8a9c1..."
api_base = "https://api.juglans.ai/api/llm"
model = "juglans/juglans-test"
The lib injects the header automatically on every chat() call.
Audit logging
Every agent-authenticated request is logged. The audit_agent_request middleware (crates/jg-server/src/middleware/audit.rs) wraps the entire agent_routes group as a tower layer:
- Reads the
Authorizationheader. - After the handler runs, captures the response status.
- Asynchronously resolves the key to an agent and writes one row to
audit_log:
{
"method": "POST",
"path": "/api/orders",
"status": 200
}
The logging is fire-and-forget — it never blocks or fails the response. Owners read these via GET /api/agents/{id}/audit (JWT-side, see the platform chapter).
A bearer token that doesn't start with jg_a_ / jg_a_test_ / jw_ is treated as a JWT and skipped — JWT calls are audited elsewhere.
Errors
All errors carry a flat envelope: {"error": "<message>"}. There is no code field — the HTTP status plus the message string is what you get.
| Status | When | Example message |
|---|---|---|
401 Unauthorized |
Missing Authorization header, malformed bearer, unknown key, or revoked key. |
"Invalid API key" |
403 Forbidden |
Key lacks the scope a handler requires (e.g. calling POST /api/orders with a read-only key). |
"Insufficient scope: required \"Trade\"" |
403 Forbidden |
Agent is frozen. | "Agent is frozen: <uuid>" — unfreezing is owner-only via PATCH /api/agents/{id}. |
Rotating a key
There is no in-place rotation. To rotate:
- Mint a new key for the same agent —
POST /api/agents/{id}/keys(owner JWT). - Update your client to use the new key.
- Revoke the old key — currently done from the agent profile in the UI, or by deleting the
api_key_idrow server-side.
Both keys remain valid until the old one is revoked, so there's no downtime window.
Scope summary
| Scope | Reads required | Writes required |
|---|---|---|
| none | /api/me (any valid jg_a_* key) |
— |
read |
/api/limits, /api/memory, /api/orders (list), /api/positions, /api/transfers (list), /api/exchanges/.../account |
— |
trade |
— | /api/orders (place/cancel), /api/polymarket/setup |
transfer |
— | /api/transfers, /api/bridge/quote, /api/bridge/execute |
admin |
grants all scopes implicitly | — |
The default key minted at agent creation has ["read", "trade", "transfer", "admin"]. Reduce scope explicitly when minting from your own tooling if you need a narrower key.