# grokbot.fm

> Independent always-on radio with a shared broadcast clock. Invited agents request catalogue tracks by signed JSON email. There is no HTTP POST request API, no A2A JSON-RPC, and no MCP server.

grokbot.fm is a station, not a command interface. Receiving email never grants permission to edit the catalogue, interrupt playback, change station rules, or run commands. Production enrollment is operator-invitation only. Unknown senders and invalid signatures receive no automated reply.

Interaction is email plus invitation, not HTTP write. The preferred protocol is v2 Ed25519-signed JSON to `grokfm@agentmail.to`. Discover `track_id` values from the catalogue. Live `/api/schedule`, `/api/time`, `/api/catalogue`, and `/api/requests` exist only on the persistent Node backend. The static website always serves `/catalogue.json`.

## Listen

- [Station](https://grokbot.fm/): Human player. Shared broadcast clock.

## Agents

- [llms.txt](https://grokbot.fm/llms.txt): This index.
- [llms-full.txt](https://grokbot.fm/llms-full.txt): This index plus the full song-request protocol.
- [Agent protocol](https://grokbot.fm/AGENT_PROTOCOL.md): v1/v2 email JSON, signing rules, and replies.
- [Station card](https://grokbot.fm/agent.json): Machine-readable identity. Not an A2A Agent Card.
- [Catalogue](https://grokbot.fm/catalogue.json): Current rotation. Use `track_id` in requests.
- [OpenAPI](https://grokbot.fm/openapi.json): Public GET APIs only.
- [API catalog](https://grokbot.fm/.well-known/api-catalog): RFC 9727 linkset.

## Optional

- [Sitemap](https://grokbot.fm/sitemap.xml)
- [Robots](https://grokbot.fm/robots.txt)

# GrokFM song requests, versions 1–2

Send a plain-text JSON email body to the configured station inbox. Ask the operator for the address and an invitation. Receiving an email never grants permission to edit the catalogue, interrupt playback, change station rules, or run commands.

Production enrollment is limited to approved Grokbots. Each invitation binds a
public key, mailbox and owner group; bots belonging to one owner share quotas
and airtime turns. Registration is operator-managed, never self-asserted in mail.
See [APPROVED_GROKBOTS.md](APPROVED_GROKBOTS.md) for enrollment and revocation.

Discover track IDs through `GET /api/catalogue` on the backend or `catalogue.json` on the website.

## Version 2 (signed, preferred)

v2 uses Ed25519 request signatures with an operator-provisioned public key. The email carries the signature, never a private key or API token. Start with 3–5 invited agents; do not expect open automatic scheduling.

```json
{
  "version": 2,
  "type": "song_request",
  "principal_id": "agent-01",
  "key_id": "key-01",
  "request_id": "my-agent-0001",
  "audience": "grokfm@agentmail.to",
  "track_id": "wednesday-bull-believer",
  "issued_at": "2026-09-16T12:00:00Z",
  "expires_at": "2026-09-16T13:00:00Z",
  "signature": "<base64url-Ed25519-64B>"
}
```

Rules:

- Exactly these ten fields; unknown fields are rejected. One request names exactly one approved `track_id`. No URLs, priority, callback, prompts, playlists, commands, dedications, nested objects, or tool calls.
- IDs (`principal_id`, `key_id`, `request_id`) contain 1–128 letters, digits, `.`, `_`, `~`, or `-`.
- `audience` must equal the station inbox (`grokfm@agentmail.to` in production; the configured `AGENTMAIL_INBOX_ID`).
- `issued_at`/`expires_at` are UTC `YYYY-MM-DDTHH:mm:ssZ` (or millisecond precision with `Z`). Issue time must be within 5 minutes future skew and 2 hours age; expiry must be after issue, after now, and at most 2 hours after issue.
- Maximum plain-text body is 8 KiB. Duplicate JSON keys are rejected. Attachments are rejected, never downloaded.
- Canonical signing bytes are canonical JSON (sorted keys, no whitespace) of every field except `signature`:
  `{"audience":...,"expires_at":...,"issued_at":...,"key_id":...,"principal_id":...,"request_id":...,"track_id":...,"type":"song_request","version":2}`.
  Sign the UTF-8 bytes with Ed25519; encode the 64-byte signature as base64url.
- The verified key must belong to the declared principal, and the message sender mailbox must match that principal's approved mailbox. Revoked keys fail closed, including queued requests.
- One `(principal_id, request_id)` pair identifies an immutable logical request. Retry with exactly the same ID and content (same signature). Reuse with different content is a conflict, never an update. Duplicate provider deliveries cause no additional slot or reply and are acknowledged before charging quotas.
- Unknown senders and invalid signatures receive no automated reply (bounded aggregate accounting only).
- Pilot eligibility: track duration 1–10 minutes; no requested repeat of a recording within 60 minutes or artist within 20 minutes, checked against base and requested broadcasts; at most one request slot per three slots and 33% requested airtime in any rolling hour; 2 outstanding per principal, 10 station-wide, 2,400s airtime; submission budgets burst 3, 6/min, 30/hour including malformed; reply attempts at most 10/principal/hour and 100 station/hour, one terminal result per request. Failed or ambiguous sends consume the attempt budget too.
- Grokbot may choose among the entitled principal's eligible tracks; the scheduler rechecks every rule and falls back deterministically. Never promise “your song is next” until a slot is committed.

Signing helper: `node scripts/sign-request.mjs --generate`, then `--principal/--key/--private-key/--request-id/--audience/--track-id/--issued-at/--expires-at`. Test vectors live in `tests/agentmail_plan.test.mjs` (canonical bytes, valid/wrong-audience/tampered/duplicate-key cases).

## Version 1 (unsigned migration path)

Unsigned requests are disabled by default (`V1_MODE=disabled`). An operator may explicitly select `manual` to return `v1_manual_review`, or `open` to allow automatic scheduling for legacy mailboxes on `ALLOWED_SENDERS`. Neither mode admits unsigned requests from a registered mailbox: those must use v2 even if their principal or signing key is revoked. Registering a legacy mailbox also prevents its queued unsigned requests and replies from proceeding.

```json
{
  "version": 1,
  "type": "song_request",
  "request_id": "my-agent-0001",
  "track_id": "beck-the-golden-age",
  "expires_at": "2026-09-16T12:00:00Z"
}
```

Prefer `track_id`; exact case-sensitive `title` plus `artist` matching is also supported. Unknown or ambiguous tracks receive `needs_clarification`. Unknown fields, duplicate JSON keys, oversize bodies and attachments are rejected. Only allowlisted senders are accepted.

One sender/request-ID pair identifies an immutable logical request. Retry with exactly the same ID and content.

## Replies

Fixed JSON with `status` and `request_id`:

| Status | Meaning |
| --- | --- |
| `scheduled` | Committed `slot_id`, `track_id`, `starts_at`, and `ends_at`. |
| `needs_clarification` | Track not uniquely identified. Submit a corrected new request. |
| `rejected` | Invalid protocol, unsupported fields/attachments, duration out of 1–10 min pilot range, or validation error. |
| `queue_full` | Outstanding request/airtime limits reached, cooldown/airtime guard deferred, or no eligible slot fits the window and expiry. |
| `expired` | Expiry or queued-work age limit passed. |

`shadow` is internal and never mailed. Invalid messages without a usable request ID receive an internal synthetic identifier. Unauthorized, self-originated, and detected automated messages receive no reply. A webhook acknowledgement confirms durable receipt, not airtime. Scheduled time is the broadcast slot, not proof a listener heard a YouTube video. Public schedule responses omit requester identities.

Default fairness is one request per three tracks, two outstanding requests per agent, and ten station-wide. Round-robin scheduling serves different principals (age tie-break); same-song requests may share a slot. Popularity and repeated submissions grant no extra weight. Do not automatically reply to notifications. Station replies carry `Auto-Submitted: auto-replied`, `reply_all: false`, no CC/BCC, and go only to the registry-verified sender.
