clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

dependabot/npm_and_yarn/npm-f01a459b0f
bindws / docs / 13-scripts-and-agents.md
13-scripts-and-agents.md 17.9K rendered
1---2title: Scripts and agents3audience: integrator4---56# Scripts and agents78A relay for a script, an agent or a service, from nothing to handover, without a browser. Everything the console does is a signed HTTP call, so a key is all a program needs.910## The path11121. **Lease a name.** `POST https://bind.ws/lease` answers with a memorable name, its websocket URL, its console URL and when it expires (14 days). Anyone can read and write to a lease. Sign the request with your key (NIP-98, below) and the lease is reserved: only that key can claim it.132. **Claim it.** Send the `claim` method (NIP-86, below) to the relay's console URL. The relay converts in place: the events stay, your key is the owner. A lease starts with open writes and a 14-day keep-for rule; `resetrules` restores the defaults, or `applypreset` sets a bundle.143. **Publish and query.** `POST /events` with an event, `POST /query` or `POST /count` with a list of filters, each signed with NIP-98. The signer has the same standing as an authenticated socket, so the owner's own reads pass any read rule.154. **Bring history in.** `backfill` pulls your own events from the relays in your kind 10002 stored here, or from a list you give. `pullfrom` copies another relay. `listjobs` and `pullstatus` report on them.165. **Read the summary.** `GET /card.json` is the machine-readable state of a name: owner, rules, fuel, member count when public and the group address. `GET /card.nostr` is the same, signed by the relay's key.176. **Hand it over.** `transferowner` gives the relay to a member, so an agent can set a relay up and pass it to its human. The old owner stays on as a moderator.1819The cheap way to learn a community is its views: `GET /view/profiles` is every member's name and picture in one signed record, `GET /view/relays` is where those members also publish, and `GET /view/zaps` says what the place values. Each is one request and no websocket; the relay's information document lists which views it keeps.2021## Exercise a relay network in production2223`npm run test:network` runs a finite, manually invoked production exercise. It24creates five small relays, gives run-owned identities, records the topology,25exercises the relationships, extracts a report and deletes the relays it26created. It has no timer, workflow or standing job outside the run.2728The topology is deliberately small and represents different data-flow shapes:2930| Role | Relationship | Activity and size assumption |31|---|---|---|32| `peer-a`, `peer-b` | equal peers, each pulls from the other | short bursts, idle catch-up and repeat sync; both are small |33| `satellite` | pulls selected public profiles from `relay.damus.io` and `nos.lol` | a small node samples two much larger external sources |34| `personal` | exchanges selected notes with the peers, sends public tasks to `hub` and pulls results back | low-volume personal activity |35| `hub` | receives tasks and publishes results for `personal`; pulls selected profile context from `satellite` | a small agent-facing relay |3637The task and result path is a public relay workflow. It does not represent38confidential agent computation. The encrypted kind-4 fixture stays on the39personal relay, and the exercise checks that it is not copied to the hub.40External sampling is read-only and bounded to three sampled profile authors per source and one retained kind-041profile per author. Each sync job has a 120-second wait budget; the run has a4220-minute scenario budget, with cleanup allowed to finish afterward. A public43source can be empty, unavailable or rate limited, so its result is reported as44observed or inconclusive rather than treated as a capacity claim. The two45external relays are treated as larger than this network; their size is not46measured.4748Start with an offline plan, then run the exercise. The default output directory49is private state under the user's local application data directory. A supplied50directory must be new and is created with mode `700`.5152```53npm run test:network -- plan54npm run test:network -- run [new-output-directory] [domain]55npm run test:network -- cleanup <output-directory>56```5758The run writes a private `manifest.json` and `report.json` in that directory;59the manifest contains the run keys and is mode `600`, so it must not be60committed or shared. The manifest is written before the first claim and after61each state change. The `finally` cleanup extracts final `stats` and storage62inventory, then verifies each relay's owner before calling `deleterelay`. If a63process loses a claim reply or stops before cleanup, run `cleanup` against the64same directory after checking that the manifest is the intended run. Cleanup65only follows the generated names and matching owners, and reports any relay it66could not delete. Baseline fuel snapshots follow provisioning; final snapshots precede deletion.67Their deltas cover the exercise and observation calls, excluding provisioning68and teardown. The full final counters also contain provisioning usage. These69are tenant meters, not a provider invoice. The report includes signed synthetic70fixtures, job results, per-source coverage and operation-lock retry counts.71A members-only source refuses an unauthenticated pull; membership alone does72not supply a job with credentials.7374The checks cover bidirectional peer convergence, deduplication, idle catch-up,75filtered task and result flow, private-event isolation, unauthorized writes and76management, a refused members-only pull, and separation from external profile77traffic. One-shot server jobs are removed after each check. The exercise is a78bounded correctness and relationship test, not a load test or evidence of79large-relay capacity.8081## Signing a request (NIP-98)8283Every management call and every door that needs a key takes an `Authorization: Nostr <base64 event>` header. The event is:8485| Field | Value |86|---|---|87| `kind` | 27235 |88| `created_at` | now; the relay allows 60 seconds either way |89| tag `u` | the full URL, host and path must match the request |90| tag `method` | the HTTP method |91| tag `payload` | the hex SHA-256 of the body, required whenever there is a body |9293Encode the signed event as base64 JSON. The relay answers `401` with an `auth-required:` reason when any of that is off.9495With curl and [nak](https://github.com/fiatjaf/nak):9697```98BODY='{"method":"claim","params":[]}'99TOKEN=$(nak event -k 27235 -t u=https://brave-otter.bind.ws/ -t method=POST \100 -t payload=$(printf '%s' "$BODY" | shasum -a 256 | cut -d' ' -f1) --sec "$NSEC" | base64)101curl -X POST https://brave-otter.bind.ws/ \102 -H "Authorization: Nostr $TOKEN" -H "content-type: application/nostr+json+rpc" -d "$BODY"103```104105With node and nostr-tools:106107```js108import { finalizeEvent, generateSecretKey } from "nostr-tools/pure";109import { getToken } from "nostr-tools/nip98";110111const sk = generateSecretKey();112const sign = (e) => finalizeEvent(e, sk);113114// A reserved lease: the signature names the key that may claim it.115const leaseURL = "https://bind.ws/lease";116const lease = await (await fetch(leaseURL, { method: "POST", headers: { authorization: await getToken(leaseURL, "POST", sign, true) } })).json();117118// Management calls go to the relay's root as JSON-RPC.119async function rpc(method, ...params) {120 const body = { method, params };121 const token = await getToken(lease.console, "POST", sign, true, body);122 const r = await fetch(lease.console, { method: "POST", headers: { authorization: token, "content-type": "application/nostr+json+rpc" }, body: JSON.stringify(body) });123 return r.json();124}125126await rpc("claim"); // { result: { owner, claimed: true, converted: true } }127await rpc("applypreset", "outbox"); // one call sets writes, reads, kinds and keep-for128await rpc("backfill", ["wss://relay.damus.io", "wss://nos.lol"]);129console.log(await rpc("listjobs"));130console.log(await rpc("gitstorage", "<owner hex pubkey>", "my-project"));131```132133The bridge takes the same header. `POST /events` answers `{ event_id, accepted, message }`; `POST /query` answers a list of events; `POST /count` answers `{ count }`. The bridge is rate limited per address at four times the per-connection allowance and answers `429` past it.134135## Management calls (NIP-86)136137`POST /` with `content-type: application/nostr+json+rpc` and a body of `{ "method": "...", "params": [...] }`. Answers are `{ result }` or `{ error }` with a reason that starts with `invalid:`, `restricted:`, `unsupported:` or `error:`. Each method needs an action; the owner has every action and a moderator has read, members, ban, delete events, invites and reports. `supportedmethods` lists the names.138139**Open to anyone**140141- `claim`: take an unclaimed relay, or convert a lease you hold. Answers the owner and `converted: true` when it was a lease.142143**Read**144145- `stats`: counts, connections, name, owner and fuel.146- `getpolicy`: the whole policy.147- `listpresets`: preset names, descriptions, whether one needs a source, and the Connect fold's shortcuts a preset sets when it has them.148- `listconnectiontemplates`: the Connect fold's library, each template with `available`, whether the feature it needs is on.149- `listconnections`: the owner's shortcut list, or the defaults while there is none.150- `listmembers`, `listpeople`, `listallowedpubkeys`: the member list in three shapes.151- `listbannedpubkeys`, `listbannedevents`, `listblockedips`: the bans.152- `listinvites`: detailed invite records, including expired and exhausted entries; a member under the invite rule sees only their own.153- `listclaims`: up to 200 usable invite code strings; a member under the invite rule sees only their own. See [NIP-86 membership claims](24-nip86-claims.md) for the pinned proposal and constraints.154- `listrecentevents [limit]`: the newest events.155- `listallowedkinds`, `listblockedkinds`, `listretention`: the kind and keep-for rules.156- `listblobs`, `listreports`: files and the reports queue.157- `listaudit [before]`: the moderation log, newest first, 100 rows; pass the last row's `seq` for the next page. Every method that changed something is in it with the caller, the first string parameter as the target and the rest as detail.158- `successionstatus`: last seen, days silent, warning state and the handover log; the heir may read it too.159160**Members**161162- `setmember pubkey {name?, note?, role?, keepDays?, maxBytes?}`: add or edit. Only the owner sets roles and limits.163- `allowpubkey pubkey reason`: the NIP-86 spelling of add.164- `removemember pubkey`, `unrulepubkey pubkey`: remove a member or lift a ban.165- `removesubtree pubkey`: remove a member and everyone they invited, stopping at moderators.166167**Ban**168169- `banpubkey pubkey reason`, `banevent id reason`, `allowevent id`.170- `blockip address reason`, `unblockip address`.171- `setblockedwords [words]`: the list, plain words or `/patterns/`; a pattern that does not compile fails the call with `invalid:`. `setpolicy {blockedWordsInTags: true}` searches tag values too.172173**Delete events**174175- `deleteevent id`: remove one event.176177**Invites**178179- `createinvite ttlSeconds maxUses note`: mint a code. A member under the invite rule may mint within their quota.180- `revokeinvite code`.181- `createclaim code`: mint a chosen code with the default three-day lifetime and unlimited uses; returns `true`. The same member quota and depth apply.182- `deleteclaim code`: revoke a code from the shared invite store; returns `true`. Members may only delete their own codes.183184**Reports**185186- `resolvereport id action`: dismiss, delete or ban.187- `listeventsneedingmoderation`: one entry per open reported thing, event id or blob hash.188189**Rules**190191- `setpolicy {…}`: any policy fields; the relay drops values it does not accept. `views` is a map of view name to `off`, `write`, `hourly` or `daily` (`true` and `false` still mean the default trigger and off). `features` is a map: `search` is `prose`, `full` or `off`; `sync`, `count`, `discovery`, `names`, `files`, `pages` and `signer` are booleans (see Relay configuration, Rules).192- `allowkind k`, `disallowkind k`, `unrulekind k`.193- `setretention kind|null days`, `purgekind kind|null days`.194- `resetrules`: the defaults, kind rules and keep-for cleared.195- `applypreset name {source?}`: a bundle; `search` needs a source, `articles` takes one.196- `setconnections [list]`: the Connect fold's shortcuts, the whole list in the order shown, each `{template, visibility?, title?, about?, inputs?}`, up to 24; an entry that does not fit fails the call with `invalid:` and the reason. See [Connection templates](17-connection-templates.md).197198**Identity**199200- `changerelayname`, `changerelaydescription`, `changerelayicon`: one string each.201- `adddomain host`, `checkdomain host`, `removedomain host`, `listdomains`: custom hostnames, when the host has them on.202- `notifytest`: a test message to the owner.203204**Storage**205206- `storagestats`: bytes by kind, files, retention.207- `gitstorage owner identifier`: an owner-only inventory of one accepted GRASP repository. The result reports compressed and raw Git object bytes, metadata bytes, ref and receipt counts, and the relay's shared physical SQLite size. The byte breakdown is diagnostic, not another fuel charge. It never deletes data.208- `deleteblob sha256`.209- `listdumps`, `dumpnow`, `deletedump name`.210- `backupnow [id]`, `listbackups`, `deletebackup id`: portable archives; owner NIP-98 downloads use `/backups/id`. Pass `{ "id": "name", "includeGit": false }` to `backupnow` when Git is too large for the bounded archive; recover large repositories separately with `git clone --mirror` or `git bundle`. POST the exact archive with a payload-bound NIP-98 signature to `/backups/preview`, then separately sign `/backups/restore` on a fresh target. Both require the archived owner key. Archives are capped at 8 MiB and 12,000 entries.211- `listlisthistory`: private older list versions belonging to the authenticated owner, moderator or member.212- `restorelist eventId`: an unsigned `draft` and `diff` (added tags, removed tags, content changed) for one saved version belonging to that signer. Sign the draft in the client and publish it normally.213214**Config**215216- `exportconfig`, `importconfig document [{dryRun: true}]`: rules, identity, members, bans, address blocks, kind rules, retention and the Connect fold's shortcuts as one document (format `bind.ws/relay-config/2`). A section the document leaves out is left alone. With `dryRun`, the answer is `{ changes, warnings }`: what applying would change, section by section with a `summary` line each, and the entries the relay would not take; nothing is applied or logged.217218**Jobs**219220- `pullfrom url`, `pullstatus`: copy one relay and follow it.221- `addjob {kind, relays, filter?, every?, label?}`: a `pull` or `push`, once or every 1, 6 or 24 hours. Up to 10 relays; filters take up to 50 authors and 50 kinds and a `since`.222- `removejob id`, `runjob id`, `listjobs`. Pull jobs expose `pullSources` while running and `last.sources` after finishing: each source carries its URL, mode, status, stored/skipped/blob counts, retry count, error and coverage warning. Query window progress is persisted with the job.223- `deliverystatus`: recent automatic NIP-65 per-event target status, attempts and last error. Enable with `setpolicy {delivery: {enabled: true, maxTargets: 8}}`.224- `backfill [relays?]`: your own events from your kind 10002 here, or from the list.225226**Transfer**227228- `transferowner pubkey`: to a member; you stay as a moderator.229- `setsuccession {heir, afterDays}` with 90, 180 or 365, `clearsuccession`.230231**Fork**232233- `forkrelay {name?, holder?, filter?, people?}`: a new leased name filled from this relay, reserved for the holder. One an hour.234235**Delete**236237- `deleterelay name`: the relay's own name, typed, is the confirmation.238239`gitstorage` is a diagnostic read, not a billing ledger or a deletion grant.240Its operation counts do not add a tenant charge, and its 60-second cooldown is241per live Durable Object instance, so eviction or restart resets it.242243## The configuration file244245`exportconfig` is a document, and a document can live in a repository. The format is `bind.ws/relay-config/2`: `policy`, `members`, `bans`, `addresses`, `banned_events`, `kinds`, `retention` and `connections`, each a section, and a section the file leaves out is left alone when it is applied, so a file may carry the rules and nothing about people. `connections` is the Connect fold's shortcuts, in order, each naming a template from the repository's `connection-templates/` folder with who sees it and the template's inputs ([Connection templates](17-connection-templates.md)). The schema is at `https://bind.ws/relay-config.schema.json`; put it in `$schema` and the editor checks the file as you type. JSONC is fine: comments and trailing commas.246247Four verbs, run from a checkout of the repository with `RELAY_SK` holding the owner's key as hex or nsec:248249```250npm run relay check relay.jsonc # the schema and the relay's own parser, offline251npm run relay plan relay.jsonc wss://name.bind.ws # what applying would change; touches nothing252npm run relay push relay.jsonc wss://name.bind.ws # the plan, then the change253npm run relay pull wss://name.bind.ws relay.json # the relay as it is, into a file254```255256`check` fails on anything a relay would drop, since a silently dropped entry is a mistake in a file under source control. `plan` is `importconfig` with `{ dryRun: true }`: the answer lists the changes section by section with a `summary` line each, plus the entries the relay would not take. `pull` then `git diff` shows drift between the file and the relay.257258The templates in the repository's `relay-templates/` folder are files in this format with a `template` block for the title and the blurb; the console offers them as presets. A template is a good start for a file of your own.259260## Limits worth knowing261262- Leases: five a minute per address and 60 a minute overall at the apex.263- Bridge and sockets: the relay's events and queries per minute, per connection, and four times that per address.264- Fuel: a lease cannot be zapped and runs on the free allowance only. A claimed relay past an allowance with no balance goes read-only until zapped; `GET /fuel` reports the meters.265- A relay only pulls from sources that let anyone read, and only pushes to targets that accept its key's writes.