Photo drafts

Draft photos and notes are sent to Vercel AI Gateway and a hosted model provider. We request that they are not used for model training. Provider retention and abuse-monitoring policies still apply; this public deployment does not promise zero retention across those services. Barter processes draft photos in memory and does not save draft text or photos on the server. Posting to the board saves the public listing and its photos.

About the data

The board is a cache of signed public records. This page says what is public, what is private, and how anyone can read and check it.

In plain words

Read API

Free, no sign-in, open to any website (CORS *). Every list is paginated with an opaque cursor over (created_at, id); pass back the next value until it is null. limit is at most 100. Same-second events are never skipped.

GET /api/board/listings?type=have|need&t=<category>&area=<suburb>&cursor=&limit=
Current listings, newest first: {items:[{event, naddr, derived}], next, profiles}. derived holds the status, the current interaction and, for every interaction, whether both people confirmed.
GET /api/board/listings/<coordinate>/history?cursor=&limit=
Everything public about one listing, oldest first and paginated, including superseded, expired and withdrawn revisions: reservations, cancels, claims (labelled bound or unbound) and responses, plus next. Every page carries the same derived, worked out over the whole listing, never over the page. A reservation belongs to the listing in its a tag; a cancel, claim or response belongs wherever the reservation it names belongs (a claim's own a tag never moves it), and to no listing while that reservation isn't stored. The coordinate is 30402:<pubkey>:<d>, URL-encoded.
GET /api/board/events/<id>
One stored public event, unmodified.
GET /api/board/raw?cursor=&limit=
The archive: every stored public, non-hidden event, unmodified, oldest first: {events, next}.
GET /board/feed.xml and /board/feed.json
RSS 2.0 and JSON Feed of current listings, with the same type, t and area filters.
GET /media/<sha256>.jpg
A photo, served while at least one current, non-hidden listing uses it.
GET /api/board/info
The public base URL signatures are checked against, server time and the category list.
GET /api/board/profiles?pubkeys=<hex>,…
The latest visible public name (kind 0) for up to 50 keys.

Writes: POST /api/board/events takes one signed public event. Photos go to POST /api/board/media, private messages to POST /api/inbox/send, reports to POST /api/board/reports; all three use NIP-98 HTTP auth.

Records

Every public record is a Nostr event signed by the person's own key, carries ["v","barter/1"], has at most 4 KB of content and 30 tags, and a created_at within ten minutes of server time. The server never holds, creates or uses anyone's key, and never signs on anyone's behalf.

RecordKindSigned byKey tags
Profile (display name)0the personcontent {"name":…}
Listing (have; a need adds ["type","wanted"])30402 (NIP-99)ownerd, title, summary, price or terms, condition (optional), t (category slug), ["l", suburb, "barter.area"], image (≤ 4, this board's media only), status active|reserved|taken|withdrawn, expiration (NIP-40, default 30 days); reserved/taken also ["e", reservation id, "", "reservation"]
Reservation7780listing ownera (listing), e (listing revision), p (counterparty), giver, receiver. Its event id is the interaction id.
Cancel / release7781either participante (interaction), p (other), reason released|cancelled
Pickup claim30780a participantd = <interaction id>:<role>, e, a, p (other), role, outcome completed|no_show
Response to a claim7782the other participante (claim event id), e (interaction id), p (claim author)

Kind numbers 7780–7782 and 30780 are proposals chosen for this board; they are not registered. Private messages are NIP-17 gift wraps (kinds 14, 13, 1059) and are never public. Reports (1984) and NIP-98 auth events (27235) are never stored as public records.

Which version of a listing is current

For each (kind, pubkey, d) the latest created_at wins, and on a tie the lowest event id. That is decided first; only then are expired, withdrawn and hidden versions dropped. An older active version never comes back when a newer one has expired or been withdrawn. Current endpoints list only current, unexpired, non-withdrawn, non-hidden versions; the archive keeps the rest.

Pickups: how interactions are derived

An interaction is a reservation event R. R counts only if it is signed by the listing owner, its a tag is that owner's listing, its e tag is one of that listing's revisions, giver and receiver are different keys, the owner is the giver (a have listing) or the receiver (a want listing), and p is the other participant. Every attempt, even with the same person again, is a new reservation with a new id.

  1. A claim is bound only if its e is R's id, its signer is the participant R names for the claim's role, its p is the other participant, its a is R's listing, its d is <R>:<role>, and it was not made after a cancel of R.
  2. For each role the latest revision of that participant's claim for R is taken; if it isn't bound there is no current claim for that role.
  3. Both people confirmed this pickup when the current giver claim and current receiver claim are both completed, the two signers differ, and neither participant has cancelled R. The words "verified" are never used.
  4. A bound claim by itself, including a no-show, is shown as "<name> says …" with any bound response beside it. A response is bound only if it is signed by the other participant and names the exact claim revision; a response to a replaced claim stays with that older revision. A response never counts as a confirmation.
  5. Unbound claims (wrong p, wrong role, an outsider, a cancelled or unknown interaction) never count and are not shown as testimony. The API still returns them, labelled unbound.
  6. History is per interaction. A cancelled A/B, then A/C, then a new A/B are three separate interactions. Marking a listing taken or withdrawn never rebinds or erases earlier ones.

Private messages

The browser builds a kind 14 rumor, seals it (kind 13, signed by the real sender, NIP-44 encrypted to the recipient) and gift-wraps it (kind 1059) under a throwaway key, once for the recipient and once as the sender's own history copy. The wrapper carries only a p tag. created_at on wraps is randomised up to two days into the past, so the inbox is paged by the server's receive order, not by created_at. The browser checks every signature, that the seal's author wrote the rumor, the rumor id, and that the message is about a listing and addressed to (or sent by) the key reading it; anything else shows as "couldn't verify" and is not displayed.

Check it yourself

helper/scripts/read-board.mjs <base-url> walks every page of the endpoints above, verifies every signature, re-derives current versions and pickups with its own code (it shares nothing with the server) and compares them with the server's derived blocks.