# Agent Guide: Autonomous Operations on counters.dev Complete guide for AI agents to discover counters, build and maintain dashboard boards, mint scoped credentials, and self-destruct keys. --- ## 1. The Two Hosts and Authentication counters.dev splits traffic across two hosts with two credential worlds that never mix: | Host | Serves | Required Credential | |---|---|---| | `https://api.counters.dev` (Data Plane) | GET /v1/counters, values, series | Organization API key (sk_live_...) | | `https://hub.counters.dev` (Hub & Control Plane) | /v1/boards*, /v1/keys* | Organization API key (sk_live_...) | Every request carries the exact same header: ```http Authorization: Bearer sk_live_... ``` ### Strict Isolation Rules - **No Publishable Tokens:** Publishable browser keys (pk_...) are rejected with 403 Forbidden before scope checks. - **No Human JWTs:** WorkOS user session tokens on /v1/boards* return 401 Unauthorized. - **No CORS Headers:** Key-authenticated responses omit CORS headers — execute requests from CLI tools, backend containers, or agent runtime environments. - **Disabled Board Gate:** The board surface is enabled by default. If every board route answers 404 even with a valid key and scopes, an operator has disabled the organization's board gate: instruct the human operator to re-enable it. - **Rate Limits:** 20 rps per key (burst 40). If 429 Too Many Requests is received, back off per the Retry-After header (typically 1 second). - **Never Expose Secrets:** Never echo or commit the human's API key in logs, git repos, or public transcripts. --- ## 2. Scopes Matrix Every organization API key carries an explicit list of scopes: | Action | Host | Endpoint | Required Scope | |---|---|---|---| | List & discover counters | api.counters.dev | `GET /v1/counters` | `counter:read` | | List or get boards | hub.counters.dev | `GET /v1/boards, GET /v1/boards/{boardId}` | `board:read` | | Create or update boards | hub.counters.dev | `POST /v1/boards, PUT /v1/boards/{boardId}` | `board:write` | | Delete boards | hub.counters.dev | `DELETE /v1/boards/{boardId}` | `board:manage` | | Mint child API keys | hub.counters.dev | `POST /v1/keys` | `key:mint` | | Self-destruct calling key | hub.counters.dev | `DELETE /v1/keys/self` | `None (any key can self-revoke)` | Scope enforcement is **strict**: If a key lacks the required scope, the server returns `403 Forbidden` with a standard problem payload: ```json { "type": "scope_denied", "required": "board:write" } ``` The response names the single required scope. If you receive `scope_denied`, stop retrying and ask the human operator for a key with that scope. --- ## 3. Step 0: Discover Existing Counters Before creating or adding widgets to a board, list the counters available in the organization: ```http GET https://api.counters.dev/v1/counters?limit=100 Authorization: Bearer sk_live_... ``` Response: ```json { "data": [ { "key": "signups", "value": "12912", "epoch": 1 }, { "key": "active_sessions", "value": "842", "epoch": 1 }, { "key": "api_requests", "value": "450201", "epoch": 1 } ], "nextCursor": null } ``` - Follow nextCursor until null to view all pages. - Counter keys follow the format ^[A-Za-z0-9._:-]+$ (max 200 characters). - Derived counters (computed expressions over counters) can also be used as widget targets by setting "isDerived": true. - Always verify counter keys against this list to prevent creating widgets with misspelled keys that display empty data. --- ## 4. Boards & The Five Widget Types A board is a named JSON document comprising a name and an array of up to 12 widgets (total payload limit: 32 KiB): ```json { "version": 1, "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "Production Metrics Overview", "createdAt": "2026-09-21T12:00:00Z", "updatedAt": "2026-09-21T12:00:00Z", "revision": 1, "widgets": [ ... ] } ``` - **Limits**: Maximum 12 widgets per board. The entire JSON payload (name + widgets) must not exceed 32 KiB. - **Server-minted fields**: `id`, `createdAt`, `updatedAt`, and `revision` are generated by the server. Never send them in request bodies. - **Optimistic concurrency**: `revision` starts at 1 and increments by 1 on every successful update. Updates must supply the current revision in the `If-Match` header. - **Strict schema**: Unknown fields are rejected with `400 Bad Request`. ### The Five Widget Types 1. **`stat`** (Stat tile): Current counter value formatted as a prominent number. - Recommended size: `s` - `options.format`: `"compact"` (e.g. `12.9K`) or `"exact"` (e.g. `12,912`). Default `"compact"`. 2. **`ticker`** (Ticker): Current counter value in real-time ticker style. - Recommended size: `s` - `options.format`: `"compact"` or `"exact"`. Default `"compact"`. 3. **`delta-chart`** (Delta chart): Line or bar chart of per-bucket deltas over a historical range. - Recommended size: `l` (or `m`) - `options.rangePreset`: `"24h"` | `"7d"` | `"30d"` | `"90d"` | `"12mo"`. Default `"7d"`. - `options.bucket`: `"1m"` | `"5m"` | `"1h"` | `"1d"` | `"1w"` | `"1mo"`. 4. **`goal`** (Goal progress): Progress meter against a target threshold. - Recommended size: `m` - `options.target`: string representing the decimal target, e.g. `"1000000"`. **Must be supplied by the human operator; never invent target numbers.** 5. **`table`** (Recent buckets): Tabular view of recent interval values. - Recommended size: `m` - `options.rangePreset` and `options.bucket`: same as `delta-chart`. - `options.limit`: integer between 1 and 1000. --- ## 5. Creating Boards (POST /v1/boards) To create a board, send a `POST` request with an `Idempotency-Key` header: ```http POST https://hub.counters.dev/v1/boards Authorization: Bearer sk_live_... Content-Type: application/json Idempotency-Key: e1a3f01b-9351-4d43-bb11-85dae4450bf0 { "name": "Executive KPI Dashboard", "widgets": [ { "id": "w1", "type": "stat", "counterKey": "daily_active_users", "title": "Daily Active Users", "size": "s", "options": { "format": "compact" } }, { "id": "w2", "type": "delta-chart", "counterKey": "daily_active_users", "title": "DAU (Last 7 Days)", "size": "l", "options": { "rangePreset": "7d", "bucket": "1h" } } ] } ``` - **Idempotency**: Always send a fresh lowercase UUID (v4) in `Idempotency-Key`. If a network failure occurs, retrying with the same key and identical payload returns `200 OK` with the original board, avoiding duplicate boards. - **Response**: `201 Created` returns the full board object. - **Save State**: Retain the returned `id` and `revision` (starts at 1) for future operations. --- ## 6. Updating Boards & Optimistic Concurrency Updates replace the board whole. Always supply the `If-Match` header with the exact stored integer revision. ```http PUT https://hub.counters.dev/v1/boards/3fa85f64-5717-4562-b3fc-2c963f66afa6 Authorization: Bearer sk_live_... Content-Type: application/json If-Match: 1 { "name": "Executive KPI Dashboard (Updated)", "widgets": [ ...full list of up to 12 widgets... ] } ``` - **`If-Match` header is required**: Pass the exact current integer revision. - If revision is stale or mismatch occurs, the server returns `409 Conflict` (or `412 Precondition Failed`). When this happens: 1. `GET https://hub.counters.dev/v1/boards/{boardId}` to fetch the fresh revision and current widgets. 2. Merge your changes into the fresh widget list. 3. Re-issue the `PUT` request with the updated revision. 4. Never force-overwrite blindly. --- ## 7. Deleting Boards (DELETE /v1/boards/{boardId}) ```http DELETE https://hub.counters.dev/v1/boards/3fa85f64-5717-4562-b3fc-2c963f66afa6 Authorization: Bearer sk_live_... ``` - Requires the `board:manage` scope. - Returns `204 No Content`. - Deleting a board deletes only the dashboard layout; underlying counters and their historical time-series data are untouched. --- ## 8. Key Minting & Delegation Recipe Large workflows and agent fleets should never share one master key across every sub-agent. Instead, use counters.dev's key-minting delegation pattern: ```mermaid sequenceDiagram autonumber actor Human as Human Operator participant Orch as Orchestrator Agent participant Hub as counters.dev Hub participant Worker as Worker Sub-Agent participant API as counters.dev Data Plane Human->>Orch: Provide Admin Key (scopes: key:mint, board:write, counter:read) Orch->>Hub: POST /v1/keys (mint sub-key with scopes: counter:read, board:write) Hub-->>Orch: 201 Created { apiKey: "sk_live_subagent...", keyId: "..." } Orch->>Worker: Dispatch task with sk_live_subagent... Worker->>API: GET /v1/counters (discover keys) Worker->>Hub: POST /v1/boards (create board) Worker->>Hub: DELETE /v1/keys/self (self-destruct key) Hub-->>Worker: 204 No Content (sub-key permanently dead) Worker-->>Orch: Task completed opt Cleanup Orch->>Hub: DELETE /v1/keys/self (destroy admin key if temporary) end ``` ### Step 1: Mint a Scoped Key Calling key must have the `key:mint` scope. ```http POST https://hub.counters.dev/v1/keys Authorization: Bearer sk_live_admin... Content-Type: application/json { "name": "worker-agent-board-creator", "scopes": ["counter:read", "board:read", "board:write"], "expires_at": "2026-09-27T12:00:00Z" } ``` Rules: - The requested `scopes` must be an exact subset of the caller key's own scopes. A key cannot grant permissions it does not possess. - `expires_at` is optional (ISO 8601 string or null). - Response returns the new key's plaintext secret once. ### Step 2: Self-Destruct When Finished Any API key can revoke itself. No specific scope is required: ```http DELETE https://hub.counters.dev/v1/keys/self Authorization: Bearer sk_live_subagent... ``` Response: `204 No Content`. Immediately after this call returns, any subsequent request with this key will fail with `401 Unauthorized`. --- ## 9. Error Handling Reference | HTTP Status | Error Type / Problem | Cause / What The Agent Must Do | |---|---|---| | `400` | `bad_request` | Malformed body, unknown fields, missing If-Match, or malformed UUID. Fix payload against schema. | | `401` | `unauthorized` | Missing, expired, or revoked API key, or sent JWT instead of API key. Re-obtain valid key. | | `403` | `scope_denied` | Key lacks required scope. Check problem required member and ask human operator for that scope. | | `404` | `not_found` | Unknown boardId, cross-org resource, or organization board surface is disabled. If all board routes 404, notify operator that board gate is disabled. | | `409 / 412` | `conflict / concurrency` | Stale If-Match revision or Idempotency-Key conflict. Re-fetch board via GET, merge, and retry. | | `413` | `payload_too_large` | Board exceeds 32 KiB cap. Use fewer widgets (cap 12) or shorter titles. | | `429` | `rate_limited` | Per-key rate limit exceeded (20 rps). Wait according to Retry-After header and retry. | --- ## 10. Operator & Agent Checklist - [ ] Obtained an organization key with counter:read, board:read, and board:write. - [ ] Queried data plane GET /v1/counters to verify exact counter keys before adding widgets. - [ ] Generated a fresh lowercase UUID v4 for Idempotency-Key on board creation. - [ ] Maintained board constraints: maximum 12 widgets and 32 KiB size limit. - [ ] Consulted the human operator for goal widget targets (never guessed or assumed). - [ ] Used If-Match for updates and properly handled 409/412 optimistic concurrency conflicts. - [ ] Revoked temporary or sub-agent keys with DELETE /v1/keys/self upon completion.