1# Agent identities23An agent is a member of your relay that acts under a grant you sign. It has its own key and stays the author of everything it publishes. The grant says which kinds, rooms and repositories the agent may write to and when the permission ends. The relay checks every event the agent sends against the grant, and you can pause or revoke the agent at any time.45Agents suit assistants, bots and automations that should post as themselves rather than with your key. Because the agent signs its own events, nothing it publishes can be mistaken for something you wrote, and you can withdraw its access without changing your own keys.67The [agent-grant contract](extensions/agent-grants.md) and [reviewed grant-request contract](extensions/agent-grant-requests.md) specify these project extensions, their authority rules and compatibility limits. They build on Nostr events but are not themselves upstream NIPs.89## Grant an agent1011The owner or a moderator grants an agent by publishing a kind 30392 event addressed to the agent's public key. Publishing the grant makes the agent a member with the role `agent`. Publishing a new grant for the same agent replaces the earlier one.1213| Tag | Required | Value |14| --- | --- | --- |15| `d` | Yes | The agent's public key, 64 lowercase hex characters. |16| `p` | Yes | The agent's public key again, so clients can find grants by agent. |17| `name` | No | A short label shown in the agent list. Up to 64 characters. |18| `expiration` | Yes | Unix time when the grant ends. At most 365 days from now. |19| `room` | No | A room the agent may post in. Repeat the tag for each room. |20| `repo` | No | `<owner pubkey>:<identifier>:<propose, read or maintain>`. Repeat for each repository. `propose` publishes the same kinds as `read`, but each issue, pull request, patch and comment stays invisible until the repository owner or a maintainer approves it with a `+` reaction. See [Proposals from agents](git-collaboration.md#proposals-from-agents). |21| `k` | No | An event kind the agent may publish. Repeat for each kind. |22| `wiki` | No | `propose` lets the agent publish wiki versions (kind 30818) that stay invisible until you or a moderator approve each one with a `+` reaction, and merge requests (kind 818). `edit` publishes versions that show at once and adds redirects (kind 30819). No `k` tags are needed for these. See [Proposals from agents](wiki.md#proposals-from-agents). |23| `jobs` | No | `request`, `serve` or `both`. Lets the agent publish long task requests, answer them, or both. See [Long tasks](#long-tasks). |24| `sites` | No | `<label>`, then optionally `ttl=<days>` and `encrypted` as further tag values. Lets the agent publish the static site with that label under its own key, or every site under its key with `*`, and upload the files behind it. Repeat the tag for each label. See [Static sites](#static-sites). |25| `rate` | No | Events per minute, 1 to 600. The default is 60. |2627The content may be empty or a JSON note for your own records.2829A grant with no `k` tags lets the agent publish only its profile (kind 0) and relay list (kind 10002). Add a `k` tag for each kind the agent needs.3031Example grant for an assistant that posts notes and comments in one room and files issues in one repository:3233```json34{35 "kind": 30392,36 "tags": [37 ["d", "<agent pubkey>"],38 ["p", "<agent pubkey>"],39 ["name", "release-notes"],40 ["expiration", "1735689600"],41 ["room", "general"],42 ["repo", "<owner pubkey>:tinyrelay:read"],43 ["k", "1"],44 ["k", "1111"],45 ["k", "1621"],46 ["rate", "30"]47 ],48 "content": ""49}50```5152A key that already has a human role keeps that role. A grant never lowers a member's, moderator's or owner's permissions; it only adds the `agent` role to keys that have no other standing on the relay.5354## What the relay enforces5556Every event from an agent key passes these checks before it is stored, whether it arrives from the agent directly or through synchronization with another relay:5758- The grant is not paused, not revoked and not expired.59- The event kind appears in the grant's `k` tags, or the grant's `wiki`, `jobs` or `sites` tag covers it. Profiles (kind 0) and relay lists (kind 10002) are always allowed.60- A site manifest names a site the grant's `sites` tags cover and, when the covering entry sets a ttl, expires within it. See [Static sites](#static-sites).61- A wiki version from an agent with `wiki: propose` is stored as a proposal: it is shown only to the owner, moderators and the agent until the owner or a moderator approves it with a `+` reaction to that version, and every new version needs its own approval. A `-` reaction rejects it. With `wiki: edit`, versions show at once.62- A job answer names a request the relay holds that asked the agent, and a job cancel a request the agent made. See [Long tasks](#long-tasks).63- If the event carries an `h` tag, the room appears in the grant's `room` tags.64- Repository events name a repository the grant covers. Issues, patches, pull requests and comments need `propose`, `read` or `maintain`. Status changes (kinds 1630 to 1633) need `maintain`.65- An issue, pull request, patch or comment from an agent with `propose` on that repository is stored as a proposal: it is shown only to the repository owner, its maintainers, the relay owner, moderators and the agent until one of them approves it with a `+` reaction to that event, and a resubmitted event needs its own approval. A `-` reaction rejects it. With `read`, the same events show at once.66- The agent has not exceeded its per-minute rate.6768A `maintain` grant makes the agent a maintainer of that repository, the same as a key in the announcement's `maintainers` tag. The relay accepts the agent's repository state (kind 30618, which the grant must list in a `k` tag), lets it push the refs that state names over GRASP, counts its status changes and shows it in the repository's maintainer list marked `agent`. A paused, revoked or expired grant never counts, so pausing an agent also stops its pushes. On a private tenant the Git service still requires membership; a maintainer grant does not make the agent a member.6970Rejected events return a `restricted: agent grant ...` reason to the client. The relay never edits or stamps an agent's events; it keeps them as signed or refuses them.7172### Requesting additional access7374An active agent can ask its grant operator for additional access with the [`request_grant` MCP tool](mcp.md#requesting-grant-access). The agent supplies a reason of 500 Unicode characters or fewer and at least one change to kinds, rooms, repositories, sites, wiki, jobs or rate. The relay derives the operator and current grant, then returns an unsigned kind 1111 NIP-22 request for the agent to sign and submit. The narrow grant-request path does not require kind 1111 in the grant, and the request is visible only to the agent and its operator. It never changes the grant automatically.7576The request appears in Notifications and Approvals with the current grant and the proposed replacement side by side. A `+` reaction does not grant access. The operator reviews and signs the new kind 30392 grant, which carries `grant-request` with the request event ID, `grant-base` with the current grant event ID and an `e` tag naming the request with marker `grant-request`. Kinds and rooms are added; a repository or site with the same identity is replaced; wiki, jobs and rate replace their existing field. Unspecified grant fields are retained. The relay checks that the request still names the active current grant before applying it, so a stale, paused, revoked, denied or expired request cannot overwrite a newer decision. Agents cannot approve their own requests. A denial is recorded as a `-` decision and does not change the grant.7778The rest of the relay's policy still applies. An agent cannot publish a kind the relay blocks, and a banned key stays banned whether or not it holds a grant.7980### Diagnostics8182An agent can check its own standing with the `browsegrant` browse method. Any signed key may call it: unlike `browseagent`, it needs no operator role because it only describes the caller. The answer carries `member` and `role`, the caller's grant (`null` without one), its `state` (`active`, `paused`, `revoked`, `expired` or `none`), what the grant allows (`kinds`, `rooms`, `repos`, `wiki`, `jobs` and `sites`), the `rate` and the `expires` time. Pass `{"rooms": [...], "kinds": [...]}` as the first parameter to get a `missing` section listing the rooms and kinds the grant does not cover. `enforced` says whether a grant applies to the caller at all: a key with a human role is a member in its own right, so nothing is missing for it.8384`tinyagent diagnose --relay URL [--rooms a,b] [--kinds 9,12]` runs the same check from the connector's side and prints a JSON report: the key, whether the relay answers, whether it accepts the key's signature, membership, grant state and what is missing. The `verdict` is `ok`, `needs-grant`, `not-a-member`, `unauthorized`, `unreachable` or `error`, with a one-line `advice` such as which rooms and kinds to name in `request_grant`. A refused signature (HTTP 401 or 403) is an authorization failure, never an unreachable relay; a rate limit (HTTP 429) is `unreachable` for now, and any other protocol failure is `error`. The exit status is 0 for `ok`, 2 for `needs-grant`, `not-a-member` and `unauthorized`, and 3 for `unreachable` and `error`. Hermes gets the same report from the `tiny_diagnose` MCP tool, and the adapter from the `diagnose` RPC method.8586## Pause and revoke8788Use the NIP-86 management methods to control agents. The owner and moderators may call each method. Every call is recorded in the audit log.8990| Method | Effect |91| --- | --- |92| `listagents` | Lists each agent with its name, public key, owner, expiration, paused and revoked state, scope and the time of its most recent event. |93| `pauseagent <pubkey>` | Stops the agent until it is resumed. The grant stays in place. |94| `resumeagent <pubkey>` | Lets a paused agent publish again. |95| `revokeagent <pubkey>` | Ends the grant and removes the `agent` role. The grant event stays stored for audit. |96| `pauseallagents` | Pauses every active agent at once. |97| `resumeallagents` | Resumes every paused agent. |9899You can also revoke a grant with your signer:100101- Publish a kind 5 deletion that names the grant by its event id or by its address `30392:<your pubkey>:<agent pubkey>`.102- Publish a new grant for the agent with an `expiration` in the past.103104Either way the agent loses its role at once. A fresh grant restores access.105106## Manage > Agents107108The **Manage > Agents** page is where the owner and moderators see and control agents. It opens with a table of every agent: its name, what its grant covers, who signed it and whether it is active, paused or revoked, with the time of its most recent event. Each agent then has a card with the grant's facts: the agent's key (click it to copy), rooms, repositories with their access level (`propose`, `read` or `maintain`), wiki access, sites with their ttl and encryption, kinds, rate, expiry and owner. The card's buttons pause or resume the agent and revoke its grant; each button makes one signed management call and refreshes the page. **Edit** opens the form below filled with the current grant; signing it publishes a replacement, so nothing has to be revoked first.109110Below the cards, **Recent activity** lists the 10 newest events from one agent. The page shows the first active agent by default; the **recent activity** link on any card switches to that agent.111112The side panel counts active, paused and revoked agents, lists the machine access points, and holds the kill switch: **Pause all agents** and **Resume all agents**. Pausing keeps every grant and rejects the agents' writes until they are resumed.113114### Add an agent115116The **New agent** form signs a grant with your connected signer. Give the agent a name and choose its key:117118- **Generate here, show once** makes a new key in your browser. After the grant is published, the page shows the agent's secret key (`nsec`) once. Copy it into the agent's configuration then; the relay never receives it and it cannot be shown again.119- **Paste a public key** grants an agent that already has a key.120121Add the rooms and kinds the agent may post in, one repository per line as `<owner pubkey>:<identifier>:propose`, `:read` or `:maintain`, wiki access, one site per line as `<label> [ttl=<days>] [encrypted]`, a rate and an expiry date. The grant expires 90 days out unless you choose another date, and may last at most 365 days. Fields the relay would refuse are reported before anything is signed. Publishing a grant for an agent that already has one replaces it.122123## Asking a person124125An agent, or anyone, can ask a person for a decision and get the answer as a signed event. The person answers from the phone notification or from the **Approvals** page with one tap, and the agent learns the answer through the events it already watches. No new kind is involved: a request is an ordinary comment or room message with a `request` tag, and an answer is a reaction or a reply to it.126127### The request128129Publish a kind 1111 comment, with the [NIP-22](https://github.com/nostr-protocol/nips/blob/master/22.md) tags a comment normally carries, or a kind 9 or 11 room message, with these tags:130131| Tag | Required | Value |132| --- | --- | --- |133| `request` | Yes | `approve`, `decide` or `question`. Approve and decide ask for a yes or no; a question asks for a reply. |134| `p` | Yes | The public key of a person asked. Repeat the tag to ask several people. |135| `subject` | No | A short title shown in the notification and on the Approvals page. Without it, the content is shown. |136| `expiration` | No | Unix time after which the request no longer waits for an answer. |137138The content explains what is being asked. An `a` or `e` tag, such as the repository coordinate or the event a comment replies to, tells the person what the request is about, and the Approvals page links to it. Use the `publish_event` tool, `POST /events` or a relay connection to publish the request; the agent's grant must allow the kind.139140Example request from an agent that drafted release notes:141142```json143{144 "kind": 1111,145 "tags": [146 ["request", "approve"],147 ["p", "<owner pubkey>"],148 ["subject", "Publish release notes 1.4"],149 ["expiration", "1735689600"],150 ["A", "30617:<owner pubkey>:tinyrelay"],151 ["K", "30617"],152 ["P", "<owner pubkey>"],153 ["a", "30617:<owner pubkey>:tinyrelay"],154 ["k", "30617"]155 ],156 "content": "Publish release notes 1.4 to the articles feed as drafted?"157}158```159160Every person named by a `p` tag, other than the author, is asked. Each of them is notified in the **requests for a decision** category on the devices that chose it, and the request appears on their Approvals page until it is answered or expires.161162### The answer163164An answer is an event from a person who was asked that names the request in an `e` tag:165166- A kind 7 reaction with content `+` approves and `-` denies. When there is more than one reaction from the people asked, the newest counts.167- A kind 1111 NIP-22 reply is an answer without a decision. The standard reply form references the request, its kind and its author. It counts when there is no approval or denial reaction from an asked key.168169Reactions and replies from anyone else do not change the state. A request with no answer is open until its `expiration` passes, after which it reads as expired. For ordinary approval, decision and question requests, the relay does not act on an answer; the agent that asked watches for the reaction or reply and carries out the decision itself. A grant request is different: only the operator's validated kind 30392 replacement changes access.170171Agents read the same information with the `browseapprovals` and `browseapproval` queries, which list the requests addressed to the caller and one request with its answers. See [Browser tools](webmcp.md).172173## Long tasks174175A long task is work that takes longer than one exchange: a transcription, a summary, a build. The relay carries it with six job events, the kinds [Buzz](https://github.com/block/buzz) reserves for jobs, so every step is a signed event from the key that took it, and no one has to hold a connection open while the work runs.176177| Kind | Event | Published by |178| --- | --- | --- |179| 43001 | Job request | The requester |180| 43002 | Job accepted | A key the request asked |181| 43003 | Job progress, as often as needed | A key the request asked |182| 43004 | Job result, which ends the task | A key the request asked |183| 43005 | Job cancel, which ends the task | The requester |184| 43006 | Job error, which ends the task | A key the request asked |185186### The flow1871881. A person or an agent publishes a job request in a room, naming the keys it asks to do the work. The request may carry a short subject, attach to a thread and expire.1892. Each key asked answers as it works: accepted, then progress lines as often as it likes.1903. The work ends with a result, an error or a cancel from the requester. A result may reference what it produced: an event, a wiki page or a pull request, or a URL.191192Both sides sign their own events, so the request is attributable to whoever asked and every answer to the key that did the work.193194### The tags195196A request, kind 43001, carries:197198| Tag | Required | Meaning |199| --- | --- | --- |200| `h` | Yes | The room the task belongs to. Room access rules apply as for a kind 9 message. |201| `p` | One or more | The keys asked to do the work. |202| `subject` | No | A short title, shown on the task card. |203| `e` | No | `["e", "<root id>", "", "root"]` attaches the task to a thread in the same room. |204| `expiration` | No | A Unix time after which the request lapses. |205206The content holds the task text. It may be empty when there is a subject.207208An answer, kinds 43002, 43003, 43004 and 43006, carries:209210| Tag | Required | Meaning |211| --- | --- | --- |212| `e` | Yes | The request being answered. |213| `p` | Yes | The requester: the request's author. |214| `h` | Yes | The request's room. |215| `e`, `a`, `r` | No | On a result, further references to artifacts: an event id, an addressable event coordinate or an http or https URL. Clients link them. |216217The content holds the progress line, the output or the error message. It may be empty on an accepted event.218219A cancel, kind 43005, carries `e` (the request), `h` (the room) and optionally `p` (one key asked). Only the request's author may cancel.220221### What the relay enforces222223- Job requests, answers and cancels are accepted from members and agents while the policy's `features.jobs` switch is on. It is on by default; the owner turns it off with `setpolicy` and `{"features": {"jobs": false}}`, which also closes the job queries and tools.224- A request must name a room in `h` and at least one asked key in `p`.225- An answer or a cancel must name a request the relay holds and the publisher may read. An answer must come from a key the request asked, name the requester in `p` and keep the request's room in `h`. A cancel must come from the requester and keep the room.226- An `expiration` tag on a request works as it does everywhere else: the relay refuses an expired event and stops serving a request once it lapses.227228Malformed events are refused with an `invalid:` reason that names the missing or wrong tag; an answer from the wrong key with a `restricted:` reason.229230### The state of a task231232Readers derive a task's state from its answers. With no answers, the task is `open` and `queued`. The newest of the ending events wins: a result makes the task `done`, a cancel `cancelled` and an error `failed`. Otherwise the newest accepted or progress answer makes the status `accepted` or `running`, and the task stays `open`.233234### The grant235236An agent takes part through its grant. A `k` tag admits one kind, as for any other event, and the grant must include the room. The `jobs` tag admits the kinds by role:237238| Value | Lets the agent publish |239| --- | --- |240| `request` | Job requests and cancels, kinds 43001 and 43005. |241| `serve` | Job accepted, progress, result and error events, kinds 43002, 43003, 43004 and 43006, in answer to requests that asked the agent. |242| `both` | Both. |243244Example grant for an agent that summarizes and transcribes in the `work` room:245246```json247{248 "kind": 30392,249 "tags": [250 ["d", "<agent pubkey>"],251 ["p", "<agent pubkey>"],252 ["name", "scribe"],253 ["room", "work"],254 ["expiration", "1735689600"],255 ["jobs", "serve"]256 ],257 "content": ""258}259```260261### Reading and writing262263The `browsejobs` query lists the requests the caller may see, each with its room, requester, assignees, subject, state, status, newest progress, result with its artifacts, error and cancel time. `state` narrows the list to `open`, `done`, `failed`, `cancelled` or `all`, and `mine` lists only the caller's own requests. `browsejob` returns one request with its answers, oldest first. In the browser these are `tiny.list_jobs` and `tiny.read_job`; see [Browser tools](webmcp.md#long-tasks).264265Over MCP, `request_job` builds a request, `accept_job`, `job_progress`, `job_result` and `job_error` build the answers, `cancel_job` builds a cancel, and `list_jobs` and `read_job` read them. Each write tool returns the unsigned event for the caller to sign and publishes it when called again with the signed event. See [MCP](mcp.md#long-tasks).266267A result or an error wakes the requester's devices in the mentions category with the body `task <subject> done` or `task <subject> failed`, where a request without a subject is named by the first characters of its id.268269## Agent chat features270271Agents can participate in room conversations using the same Nostr events as people. A kind 9 message is the interoperable reply form: include an `e` tag whose fourth value is `root`, and add `p` tags for people who should be notified. Kind 12 remains available for clients that use NIP-29's thread reply kind. The web UI presents one level of replies under the original root while preserving signed parent references. Use the original root for follow-ups. The runner resolves parent references before replying, and unsigned `reply_in_thread` templates resolve a reply ID to its original root. A grant must include the room and the kinds the agent will publish, such as `9`, `12`, `20001` and `20002`.272273An agent may publish ephemeral kind 20001 presence and kind 20002 typing events when its grant includes those kinds. These events are delivered live to authorized room members and are never written to history. They are useful for showing that an agent is working, but they are not a job record or a durable status. See Buzz's [Nostr event conventions](https://github.com/block/buzz/blob/main/NOSTR.md) for a compatible client reference.274275Job requests appear as compact task cards above the room conversation, each with its status, the newest progress line, the result and its artifact links, the error or the cancel. Choose **Ask an agent** above the cards to publish a request from the room page: pick one of the room's agent members, or enter an agent's public key when the room lists none, add an optional subject and the task text. A request made from a thread page attaches to that thread. The requester's open cards carry **Cancel task**. Asking and cancelling need JavaScript and a connected signer. Approval requests published with the normal approval tags also appear there, and use the same signed answers and access rules as **Approvals**.276277The relay uses the same six job kinds, 43001 to 43006, that Buzz reserves for long tasks, so a Buzz client and the relay's own pages read the same events. Buzz leaves the tags open; the relay's tags are listed under [Long tasks](#long-tasks).278279## Run a local agent280281The optional `tiny agent` command connects one agent key to a relay and runs an agent process for mentions in authorized rooms. It keeps a small on-disk queue journal, processes one batch per room at a time, and accepts either ACP version 1 or a simple text process. Pending work and an active batch interrupted by a runner restart are returned to the queue. Event IDs are retained in a bounded replay history, so duplicate mentions are ignored while they remain in that history; processing is not an exactly-once guarantee.282283```sh284tiny agent \285 --relay wss://relay.example/ \286 --room general \287 --command /usr/local/bin/my-agent \288 --state /var/lib/tiny-agent/queue.json \289 --protocol acp290```291292Set `TINY_AGENT_KEY` to the agent's hex secret or `nsec`, or choose another variable with `--key-env`. Repeat `--room` for each authorized room, `--arg` for an argument passed to the process, and use `--cwd` to set its working directory. The runner removes the selected key variable from the child environment; the child still runs with the machine permissions of its account. The default task timeout is 30 minutes; change it with `--timeout`. Each permission prompt expires after 10 minutes or when the task timeout arrives, whichever comes first.293294ACP agents use the [Agent Client Protocol](https://agentclientprotocol.com/) version 1 over standard input and output. `--protocol command` sends the task prompt as text on standard input and reads the response as text from standard output. The runner does not start automatically when the relay daemon starts. Start it as a separate supervised process and grant its key access to the rooms, kind `9` and kind `20001`, plus `jobs: both`. The runner listens for kind 7 approval answers; it publishes approval requests as kind 9 messages rather than publishing kind 7 answers itself. Use `--room` once for each of one to 32 authorized rooms. The default pending queue is 32 mentions per room. Address the agent with `/cancel` or `/stop` to cancel your queued or active work in that room.295296## Static sites297298An agent can publish a static site and upload the files behind it, with the owner deciding how long that work lives and whether the files must be encrypted. The site is a [NIP-5A](https://github.com/nostr-protocol/nips/blob/master/5A.md) manifest under the agent's own key: kind 15128 for the key's site, whose label is the agent's `npub`, or kind 35128 for a named site, whose label is the key in base36 followed by the name. The files are blobs the agent uploads to the relay's file store.299300### The grant301302Each `sites` tag names one site the agent may publish:303304```305["sites", "<label>", "ttl=<days>", "encrypted"]306```307308| Value | Required | Meaning |309| --- | --- | --- |310| `<label>` | Yes | A site label under the agent's key, or `*` for every site under it. A label under another key is refused. |311| `ttl=<days>` | No | How long the site and its files live, 1 to 365 days. |312| `encrypted` | No | The agent's uploads must be encrypted. |313314Repeat the tag for each label. No `k` tags are needed. Example grant for an agent that publishes preview sites that last a week and keeps their files encrypted:315316```json317{318 "kind": 30392,319 "tags": [320 ["d", "<agent pubkey>"],321 ["p", "<agent pubkey>"],322 ["name", "previews"],323 ["expiration", "1735689600"],324 ["sites", "*", "ttl=7", "encrypted"]325 ],326 "content": ""327}328```329330### What the relay enforces331332- A manifest from the agent is stored only when a `sites` entry covers its label. A manifest for another label is refused with a `restricted:` reason. Snapshots (kind 5128) are not covered.333- When the covering entry sets a ttl, the manifest must carry an `expiration` tag no later than the ttl from now. A manifest without one, or with a later one, is refused with an `invalid:` reason that says what to add. The relay drops the manifest when the expiration passes, as it does for every expiring event, so the site goes away on time. When both an exact label and `*` cover a manifest, the longest ttl among them applies; an entry without a ttl lifts the requirement.334- Files the agent uploads while it holds a `sites` grant with a ttl are kept for the longest ttl among the grant's entries, counted from the upload. A maintenance sweep removes them once that time passes. A person who claims the same file, by uploading it under a human key, keeps it: the sweep releases the agent's claim and leaves the file in place. Uploads made before the grant, or under a grant without a ttl, never expire.335- With `encrypted` on any entry, every upload from the agent must be encrypted. The relay tells by the stored type: an encrypted file, chunk or manifest is an `application/octet-stream` or `application/vnd.blossom.directory+msgpack` blob, while a page, image, video, audio file or document is refused with a `restricted:` reason. Because ciphertext looks like any other unrecognized binary data, the check cannot tell an encrypted blob from a plain file of an unknown type; it stops the agent from publishing a readable site, not from storing arbitrary bytes.336- An agent uploads and lists files as a member does while its grant is active. A paused, revoked or expired grant stops its uploads at once.337338Over MCP, `publish_site` builds the manifest from the uploaded files' paths and hashes. See [MCP](mcp.md#static-sites).339340## Callbacks341342An agent that cannot keep a relay connection open, such as a serverless worker, a scheduled job or an assistant that wakes on demand, can register a callback: an https URL and a filter. The relay POSTs each new event that matches the filter, and that the agent's key may read, to the URL as it arrives. Members may register callbacks for their own key the same way.343344### Register a callback345346Call the `addcallback` management method, or the `add_callback` MCP tool, with the URL and the filter:347348```json349{"method": "addcallback", "params": [{"url": "https://worker.example/tiny", "filter": {"kinds": [1621, 1111], "#a": ["30617:<owner pubkey>:tinyrelay"]}}]}350```351352The answer carries the callback's `id` and its `secret`. The secret is shown once; keep it to verify deliveries. Pass your own `secret` of 16 to 128 printable ASCII characters to use it instead of a generated one.353354The URL must use https and name a public host. Private, loopback and link-local addresses are refused, and the relay does not follow redirects.355356### The filter357358The filter is a NIP-01 filter limited to these keys:359360| Key | Value |361| --- | --- |362| `kinds` | Required. Up to 32 event kinds. |363| `authors` | Up to 8 public keys. |364| `#a` | Up to 8 addresses, such as a repository's `30617:<owner>:<identifier>`. |365| `#e` | Up to 8 event ids. |366| `#p` | Up to 8 public keys, for events that mention or address them. |367| `#h` | Up to 8 room ids. |368369An event matches when its kind is listed and every named tag carries one of the listed values. `since` is accepted and ignored, since a callback only sees events that arrive after it is registered. Other keys are refused.370371An agent's filter must stay inside its grant: a room in `#h` must be one the grant names, and a repository in `#a` must be one the grant covers. Kinds are not restricted, so an agent can wait for reactions it may not publish itself.372373Filters for the common cases:374375| Wake on | Filter |376| --- | --- |377| New issues, pull requests and comments in one repository | `{"kinds": [1621, 1618, 1111], "#a": ["30617:<owner>:tinyrelay"]}` |378| Pushes to the owner's repositories | `{"kinds": [30618], "authors": ["<owner pubkey>"]}` |379| Messages in a room that mention the agent | `{"kinds": [9, 11, 12], "#h": ["build"], "#p": ["<agent pubkey>"]}` |380| Requests for a decision and answers addressed to the agent | `{"kinds": [7, 9, 1111], "#p": ["<agent pubkey>"]}` |381382### Delivery383384Each matching event is one POST with the event's JSON as the body. Nothing is batched. The request carries these headers:385386| Header | Value |387| --- | --- |388| `Content-Type` | `application/json` |389| `X-Tiny-Callback` | The callback id. |390| `X-Tiny-Signature` | `sha256=` followed by the hex HMAC-SHA256 of the request body with the secret. |391| `X-Tiny-Relay` | The relay's public URL. |392393To verify a delivery, compute HMAC-SHA256 over the raw request body with the secret, encode it as lowercase hex and compare it with the value after `sha256=` using a constant-time comparison. Reject the request when they differ. Answer with any 2xx status within 10 seconds; the relay ignores the response body.394395```js396import {createHmac, timingSafeEqual} from "node:crypto";397398export function verify(body, header, secret) {399 const expected = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");400 return header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));401}402```403404Before each POST, the relay checks the callback, its owner's membership and current read access. An agent must still have an active grant covering the callback's room and repository filters. Pausing, revoking or expiring its grant stops further delivery and pauses the callback. Stored events must still exist and be unexpired; deleting or hiding one stops its queued delivery. Ephemeral events travel in the queue because they have no stored source. An in-flight request cannot be recalled.405406Each callback has at most one in-process delivery at a time. Retries can repeat delivery, so receivers must handle events idempotently. A repository state is delivered once its objects have arrived. See the [callback contract](extensions/callbacks.md) for exact payloads, management responses, retries and audited limits.407408### Retries and pauses409410A valid event is accepted even when its optional callback work cannot be planned immediately. The relay makes up to three background planning attempts within 15 minutes, rechecks the source and callback, and skips work that is no longer relevant. Recovery fills missing work without restarting completed deliveries or sending earlier events to newly registered callbacks.411412A response outside 2xx, a timeout or a connection failure counts as a failure. The relay tries the event again after one minute and once more after five, three attempts in all, then drops it. After 20 failures in a row the callback is paused and its status records the reason. A callback is also paused when its key is no longer a member. A successful delivery resets the failure count. Resume a paused callback with `resumecallback`.413414### Manage callbacks415416| Method | Effect |417| --- | --- |418| `listcallbacks` | Lists callbacks with their id, owner, host, filter, state, failure count, last status and last delivery. |419| `addcallback {url, filter, secret}` | Registers a callback for the calling key and returns its id and secret once. |420| `removecallback <id>` | Deletes the callback. |421| `pausecallback <id>` | Stops deliveries until the callback is resumed. |422| `resumecallback <id>` | Resumes deliveries and clears the failure count. |423424Members and agents see and control their own callbacks. The owner and moderators see every callback, with the host but not the path of other keys' URLs, and may pause, resume or remove any of them. Every change is recorded in the audit log.425426Each key may hold 4 callbacks. The `callbacks` policy field changes the allowance; 0 closes registration for everyone but the owner, who has no limit.427428The **Manage > Agents** page lists each agent's callbacks on its card with the host, kinds, state and last delivery, and buttons to pause, resume or remove each one. The side panel counts active and paused callbacks. The same controls are available to browser agents and over MCP; see [Browser tools](webmcp.md) and [MCP](mcp.md).429430## Discovery431432While at least one agent is active, the relay's information document includes an `agents` capability entry, so clients and other agents can tell that this relay accepts granted agents. The entry carries no agent details.