# Elinor — protocol documentation

Elinor is a Telegram bot + Mini App that coordinates recurring community shifts
(cooking, cleaning, …) for the Valley of the Commons. **All shift data lives on
a public Nostr relay as standard NIP-52 calendar events** — the bot is just one
client. Any agent can read the schedule, watch changes in real time, and sign
people up by publishing ordinary Nostr events. This page documents everything
needed to interoperate.

## Connecting

- **Relay:** `wss://relay.commonshub.dev` (strfry; open reads, open writes)
- **Coordinator (bot) pubkey:** `3f432836bece7b0a06dcbaef023f113fdcb10f96fbf98f35dd2e3b3a3c0e2dcb`
  (`npub18apjsd47eeas5pkuhthsy0c38lwtzrukl0uc7dwa9can50qw9h9s0n2qfd`)
- **Timezone:** all shift times are local Europe/Brussels; event timestamps are unix UTC.
- **Location:** Valley of the Commons

## Data model

Two event kinds, both **addressable** (NIP-01 kinds 30000–39999: for a given
`(kind, pubkey, d-tag)` the relay keeps only the newest event — publishing a
new version replaces the old one).

Every event identifier embeds a **group id** (the Telegram chat id of a
community, a negative integer like `-5343036390`): each group is a fully
independent calendar on the same relay.

### Kind 31923 — shift occurrence (NIP-52 time-based calendar event)

One event per shift, per day, per group. Signed by the **coordinator pubkey**.

```json
{
  "kind": 31923,
  "pubkey": "3f432836bece7b0a06dcbaef023f113fdcb10f96fbf98f35dd2e3b3a3c0e2dcb",
  "tags": [
    ["d", "shift-<groupId>-<YYYY-MM-DD>-<code>"],
    ["title", "Lunch Preparation"],
    ["start", "1782376200"],
    ["end", "1782383400"],
    ["start_tzid", "Europe/Brussels"],
    ["location", "Valley of the Commons"],
    ["capacity", "2"],
    ["t", "shift"],
    ["t", "<code>"],
    ["t", "group-<groupId>"]
  ],
  "content": "Lunch Preparation shift, 10:30–12:30 (Valley of the Commons) — optional description"
}
```

- `d` — `shift-<groupId>-<date>-<code>`; `code` is a short slug per shift
  type, `[a-z0-9]{1,16}` (defaults: `mc` Morning Cleaning, `lp` Lunch Preparation, `lc` Lunch Cleaning, `dp` Dinner Preparation, `dc` Dinner Cleaning).
- `start`/`end` — unix seconds (UTC).
- `capacity` — how many people the shift needs. Not enforced by the relay;
  writers must check it (see Writing).
- Occurrences are (re)published daily ~14 days ahead. Editing a shift's
  definition republishes future occurrences with the same `d` tags.

### Kind 31925 — signup (NIP-52 RSVP)

One event per person, per occurrence. Signed by the **participant's key** (the
bot derives a keypair per Telegram user; external agents use their own key).

```json
{
  "kind": 31925,
  "pubkey": "<participant pubkey>",
  "tags": [
    ["a", "31923:3f432836bece7b0a06dcbaef023f113fdcb10f96fbf98f35dd2e3b3a3c0e2dcb:shift-<groupId>-<date>-<code>"],
    ["d", "rsvp-<groupId>-<date>-<code>"],
    ["status", "accepted"],
    ["t", "shift"],
    ["p", "<actor pubkey>", "", "changed-by"]
  ],
  "content": ""
}
```

- `status` — `accepted` (signed up) or `declined` (canceled). Canceling is
  republishing the same `d` with `declined` — never deletion, so history and
  attribution survive.
- `["p", <pubkey>, "", "changed-by"]` — optional; present when someone made
  the change on the participant's behalf (anyone may — transparency, not
  permissions, is the governance model here).

### Resolution rule

For any `(author, a)` pair, **the event with the highest `created_at` wins**;
ties break to the lexically smallest `id`. A participant is on a shift iff
their latest RSVP for it has `status=accepted`.

## Reading

Who is signed up for one shift:

```json
{"kinds": [31925], "#a": ["31923:3f432836bece7b0a06dcbaef023f113fdcb10f96fbf98f35dd2e3b3a3c0e2dcb:shift-<groupId>-<date>-<code>"]}
```

All shift occurrences of a group (next/past days):

```json
{"kinds": [31923], "authors": ["3f432836bece7b0a06dcbaef023f113fdcb10f96fbf98f35dd2e3b3a3c0e2dcb"], "#t": ["group-<groupId>"]}
```

Everything one person signed up for:

```json
{"kinds": [31925], "authors": ["<participant pubkey>"], "#t": ["shift"]}
```

Watch live changes: keep a subscription open on
`{"kinds": [31923, 31925]}` (optionally filtered by `#t`: `group-<groupId>`
for occurrences, or `#a` prefixes for RSVPs).

Discover groups: query the occurrences filter above without `#t` and collect
the distinct `group-*` hashtags.

## Writing

To sign someone up (or yourself — agents are welcome):

1. Read the occurrence (kind 31923) to get `capacity`, and the current RSVPs
   for its address; count `accepted` after applying the resolution rule.
2. If there is room, publish a kind 31925 as above with `status=accepted`.
3. **`created_at` must be strictly greater** than your previous RSVP for the
   same `d`, or the relay rejects it with "replaced: have newer event".
4. To cancel, republish with `status=declined`.

Caveats:

- Capacity is cooperative — the relay does not enforce it. Check before you
  write, and expect races to be rare but possible.
- The Telegram app displays names only for participants it knows (people who
  have interacted with the bot). An RSVP from an unknown pubkey is valid and
  counted, but shows without a name until the identity is introduced.
- Do not publish kind 31923 under your own key — occurrences are only
  authoritative from the coordinator pubkey above.

## Calendar feeds (read-only convenience)

- Per group: `https://elinor.commonshub.dev/calendar/g/<token>.ics` (token shared in the group)
- Per person: `https://elinor.commonshub.dev/calendar/u/<token>.ics`
- Single occurrence: `https://elinor.commonshub.dev/calendar/event/<d-tag>.ics`

## Using the app

- Telegram Mini App: https://t.me/ElinorOstromBot/shifts
- Add the bot to a Telegram group: https://t.me/ElinorOstromBot — it seeds the
  default shift catalog and posts an intro; `/shifts` shows the day's schedule.

*This page: [HTML](https://elinor.commonshub.dev/docs) · [Markdown](https://elinor.commonshub.dev/docs.md) · machine index: [llms.txt](https://elinor.commonshub.dev/llms.txt)*
