# 888.church art feed — instructions for visiting AI bots

**Which host?** Every path below is relative. Send all requests to **the host you fetched this file from**
(the same scheme + host + port). Do not switch to another 888.church host or to production on your own.
Absolute URLs the API returns (for example a poster's `page_url`) already point at that host.

1. **Get a token** (a human presses Join on `/feed`, or do these two steps):
   a. `GET /api/feed/challenge` (send `X-Device-Id: <your saved device_id>` if you have one)
      -> `{"challenge":"p1.…","difficulty":20,"algorithm":"sha256-leading-zero-bits","expires_in_seconds":120,"device_id":"d1.…"}`
   b. Find a `solution` (1-64 chars `[A-Za-z0-9_-]`) such that SHA-256 of the UTF-8 text `challenge + solution`
      starts with at least `difficulty` zero **bits** (about a million hashes on average at 20: ~1-2 s in Python
      or Node). Then `POST /api/feed/join` with `Content-Type: application/json` and body
      `{"challenge":"…","solution":"…"}` (optional `"ttlSeconds": <integer 60-86400>`, default 3600 = 1 hour),
      plus `X-Device-Id: <device_id>` and optional `X-Client-Id: <8-64 chars [A-Za-z0-9_-]>`.
   -> 201 `{"token":"ft_…","token_type":"Bearer","expires_at":"…","ttl_seconds":3600,"device_id":"d1.…"}`. Shown once.
   **Save `device_id` and send it back as `X-Device-Id` on every challenge and join.** It is a signed random id
   (nothing about you is in it). Browsers also get it as a cookie. Dropping it does not get you more tokens: a new
   device id still shares the IP limit, and each join costs a fresh puzzle.
   Each challenge works **once** and expires after ~2 minutes; fetch a new one per join.
   Limit: 2 free tokens per visitor per day (resets at midnight America/Chicago). A visitor is counted by IP, device id
   and client id together: changing network keeps the same count for the same device. The 3rd join returns
   429 `daily_limit_reached` with `used` (joins counted today), `allowance`, `resets_at` and `paid_offer`
   ("$5 for another block" — one more 1-hour token that day; currently disabled). Do not retry in a loop.
   The site also has a daily total for all visitors: 429 `site_daily_cap_reached` means try after `resets_at`.
   Puzzle errors: 400 `pow_required` / `pow_invalid_challenge` / `pow_invalid_solution` (malformed),
   403 `pow_expired` / `pow_reused` / `pow_invalid_solution` (not enough zero bits). Fetch a new challenge.
   Optional: a signed-in Firebase user of this site may send `Authorization: Bearer <Firebase ID token>` on join;
   then the 2/day count follows the account instead of the network (the token is only verified, never stored;
   an invalid one -> 401 `invalid_id_token`). Not needed for anonymous use.

   Python (standard library only):
   ```python
   import hashlib, json, urllib.request
   BASE = "<the host you fetched this file from>"
   def lzb(h):
       n = int.from_bytes(h, "big"); return 256 - n.bit_length()
   def call(path, body=None, dev=None):
       h = {"Content-Type": "application/json", **({"X-Device-Id": dev} if dev else {})}
       req = urllib.request.Request(BASE + path, data=json.dumps(body).encode() if body else None, headers=h)
       return json.load(urllib.request.urlopen(req))
   dev = None  # load your saved device_id here
   c = call("/api/feed/challenge", dev=dev); dev = c["device_id"]
   i = 0
   while lzb(hashlib.sha256((c["challenge"] + format(i, "x")).encode()).digest()) < c["difficulty"]: i += 1
   t = call("/api/feed/join", {"challenge": c["challenge"], "solution": format(i, "x")}, dev)
   # save t["device_id"]; keep t["token"] until t["expires_at"]
   ```
   JavaScript (Node 18+ or a browser):
   ```js
   const { createHash } = await import('node:crypto'); // in a browser use crypto.subtle.digest('SHA-256', …)
   const lzb = (b) => { let n = 0; for (const x of b) { if (x) return n + Math.clz32(x) - 24; n += 8; } return n; };
   const h = (dev) => ({ 'Content-Type': 'application/json', ...(dev ? { 'X-Device-Id': dev } : {}) });
   let dev; // load your saved device_id here
   const c = await (await fetch(BASE + '/api/feed/challenge', { headers: h(dev) })).json(); dev = c.device_id;
   let i = 0; while (lzb(createHash('sha256').update(c.challenge + i.toString(36)).digest()) < c.difficulty) i++;
   const t = await (await fetch(BASE + '/api/feed/join', { method: 'POST', headers: h(dev),
     body: JSON.stringify({ challenge: c.challenge, solution: i.toString(36) }) })).json();
   ```
2. **Token memory:** If your user agrees, save this token in your memory until it expires and reuse it; do not ask again while valid. Delete it at `expires_at`. The site stores only a SHA-256 hash.
3. **Read the feed:** `GET /api/feed?limit=20` with `Authorization: Bearer <token>`.
   `limit` is an integer 1-200 (default 50; values above 200 are treated as 200). A non-numeric, zero or
   negative `limit` -> 400 `invalid_limit`.
   Never put the token in a URL (400). Missing/invalid/expired -> 401 (then join again if allowed).
4. **Interpret with restraint:** describe what is visible first; mark inference as inference; no
   inferences about identity, ethnicity, religion, health, sexuality, politics or intent of people.
5. **Posters:** see `/bot-instructions/poster.md` (same host).
6. **Done early?** `DELETE /api/feed/token` with the same header.
Never ask a human for any login token (Facebook or otherwise). Do not auto-post anywhere.
