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
- There are no accounts, emails, passwords or phone numbers. A key made in your browser ("this device") signs everything you publish.
- Posts, reservations, pickup confirmations and replies to them are public: anyone can read them without signing up, and they stay in the signed history even after you withdraw a post. The operator can hide something; hidden records stop being served everywhere.
- Enquiries and replies are private: encrypted in your browser so the server only holds ciphertext. The server can see which key a message is for and when it arrived, not what it says.
- Reports go privately to the operator. They are never served publicly.
- Photos are re-encoded on upload (no location or camera data), stay private until a current post uses them, and are deleted after 24 hours if no post does.
- "Both people confirmed this pickup" means two keys each said so. It doesn't prove who either person is, and it isn't a trust score.
- Losing your key loses editing your posts, your private messages (past and future), and the ability to confirm pickups. Back it up from the key panel.
- Pickup in person only. No credits, no payments, no shipping.
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}.derivedholds 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
boundorunbound) and responses, plusnext. Every page carries the samederived, worked out over the whole listing, never over the page. A reservation belongs to the listing in itsatag; a cancel, claim or response belongs wherever the reservation it names belongs (a claim's ownatag never moves it), and to no listing while that reservation isn't stored. The coordinate is30402:<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.xmland/board/feed.json- RSS 2.0 and JSON Feed of current listings, with the same
type,tandareafilters. 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.
| Record | Kind | Signed by | Key tags |
|---|---|---|---|
| Profile (display name) | 0 | the person | content {"name":…} |
Listing (have; a need adds ["type","wanted"]) | 30402 (NIP-99) | owner | d, 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"] |
| Reservation | 7780 | listing owner | a (listing), e (listing revision), p (counterparty), giver, receiver. Its event id is the interaction id. |
| Cancel / release | 7781 | either participant | e (interaction), p (other), reason released|cancelled |
| Pickup claim | 30780 | a participant | d = <interaction id>:<role>, e, a, p (other), role, outcome completed|no_show |
| Response to a claim | 7782 | the other participant | e (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.
- A claim is bound only if its
eis R's id, its signer is the participant R names for the claim'srole, itspis the other participant, itsais R's listing, itsdis<R>:<role>, and it was not made after a cancel of R. - 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.
- 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. - 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.
- 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, labelledunbound. - 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.