Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The participant-pool marketplace (design note)

This note records the design decisions (2026-07-16) for the participant-pool marketplace: advertisers and survey researchers search a pool of opted-in participant profiles (demographics/interests/skills) and offer reply bounties to an eligible group to encourage responses to ads and surveys. This is the settled shape the implementation items build toward, and the record of why the alternatives were rejected. The on-chain beacon (item 43) is implemented as of v0.9.0 — see the program reference for its instructions, PDA seed, and error codes. The web surface (item 45) has since landed — see the shipped web surface; the CLI campaign tree (item 44) is in progress.

The persona and the economics

The buying side is a funded campaign wallet: an advertiser funds one wallet, then signs N bountied sends programmatically — the in-page-wallet path or a CLI batch. Bounty authoring is already settled as a direct-signed surface: each send escrows its bounty from the sender’s own wallet, and the relay stays bounty-less.

This is exactly the cohort the protocol’s economics want to charge. The standing positioning principle — the system must feel free to use and be profitable for recipients, with the cost burden on senders — maps cleanly: a participant profits twice per campaign message (postage on delivery, bounty on reply), and the campaign wallet pays every cost (message rent, postage, bounty escrow, fees).

Decision 1 — profiles are an on-chain beacon plus a sealed detail blob

A participant opts in by creating a participant beacon: a small program-owned account, one per wallet (PDA seeded on the wallet, like a mailbox). The beacon carries only:

  • a coarse tag bitmap — self-attested categories drawn from a fixed, append-only vocabulary defined in mail_model constants (interests, skills, broad demographic bands — never precise values);
  • an optional detail CID — the content address of an encrypted rich profile the participant pinned to IPFS;
  • created/updated timestamps.

Every field is fixed-size, so the borsh layout is stable and rent is a constant (the listing-account precedent). Closing the beacon reclaims its rent — opting out is free and complete: the account’s existence is the opt-in.

Coarse-by-construction is the privacy design. A wallet-linked on-chain record is public forever, so the public layer is restricted to category bits that are individually low-information; anything expressive lives only in the sealed detail blob. There is no free text on chain.

Decision 2 — detail access is mail-native key handout

The detail blob is encrypted with a random symmetric key and pinned; the beacon publishes only its CID. An advertiser who wants the detail mails the participant — paying normal postage, optionally attaching a bounty to sweeten the request — and the participant replies with the key if they choose to share.

The request/grant flow is therefore SithBit mail itself: no new protocol, no access-control machinery, and the participant is paid for the attention either way. The accepted caveat: a handed-out key can be re-shared, so the detail blob should contain nothing whose onward disclosure would be harmful; rotation is re-encrypt + update the CID.

Decision 3 — two search paths, one layout

  • Web panes search through a chain-derived account-api index route (/v1/chain/participants?tags=…), mirroring the existing listings/sales index pattern: the JWT authenticates the request, the index is global.
  • The CLI campaign tree (decision 4) is chain-direct like every other sithbit command: it scans trustlessly with getProgramAccounts + memcmp filters over the tag bitmap.

The layout rule that keeps both honest: the tag bitmap sits at a fixed account offset, so memcmp filtering works without deserializing and the index route stays a convenience, never a gatekeeper.

Decision 4 — group offers author through the CLI first

The first authoring surface is a sithbit campaign command tree run by the campaign wallet:

  1. search — filter participants by tags (trustless scan);
  2. quote — price the campaign before sending: N × (message rent + each recipient’s postage + bounty + fees);
  3. send — execute N direct-signed bountied sends.

A marketplace-pane “Participants” tab with a group-offer flow rides the same library core later — the same path alias/domain sell/buy took from CLI primitive to browser pane.

Decision 5 — the advertised price is default_postage

A participant’s participation price is their mailbox’s default_postage. Opting in means setting your default postage to what campaigns must pay — a participant is inviting cold mail from strangers, which is precisely what the default price governs. Campaigns pay through the normal frombox prepay flow.

This makes the advertised price authoritative by identity rather than by enforcement: there is no second price field to drift, no send-path ABI change, and the spam-economics stay intact — a wallet that never opted in still prices cold mail at the spam floor.

Out of scope — the operator’s campaign service

A managed campaign service (deposits, billing, dashboards, audience management) is a separate product surface an operator may build on top of these primitives. The protocol is untouched either way (recorded 2026-07-15 with the item’s creation).

Rejected alternatives

  • Off-chain-only registry — no ABI change, but operator-centralized: profiles aren’t portable across operators and campaigns must trust one index. The beacon keeps the pool a protocol surface.
  • Rich profiles on chain — permanent public demographics linked to a wallet is a privacy failure regardless of consent phrasing.
  • Per-requester re-sealing of the detail blob — private, but then the public CID serves no purpose; it collapses back to beacon-only with mail attachments, and loses the one-blob/one-pin simplicity.
  • An enforced beacon price honored by the send path — requires SendMail/frombox ABI changes and a protocol definition of “campaign send”; rejected in favor of the default_postage identity.

What would reopen this

  • Tag-vocabulary exhaustion — the bitmap filling up forces a wider field or a versioned account tier (the postoffice tiered-read precedent applies).
  • Key-resharing abuse in practice — would revisit per-requester sealing or third-party attestations for the detail layer.
  • Regulatory treatment of demographic categories — could force vocabulary changes or geographic gating of the search surface.
  • Index scale — if participant counts outgrow getProgramAccounts, a dedicated indexer (the alias/sales SQLite precedent) takes over the read path.

Implementation items spawned

Recorded as HANDOFF items, in dependency order — the beacon is the surface the other two consume:

  1. Item 43 — the on-chain participant beacon (LANDED, v0.9.0): mail_model account + tag vocabulary constants (32-byte bitmap, 24 starter tags, bits not validated on-chain), mail_program create/update/close instructions (discriminants 39–41, errors 86–90 — an additive public-ABI change, hence the MINOR bump), program tests on the compiled .so. Create requires the wallet’s mailbox to exist — the advertised price is its default_postage (decision 5).
  2. Item 44 — sithbit campaign + profile authoring in the CLI: beacon create/update/close commands, detail-blob encrypt+pin, campaign search/quote/send.
  3. Item 45 — the web surface (LANDED, v0.9.0): account-api participants index route and the marketplace pane’s Participants tab. See the shipped web surface.

The shipped web surface (item 45)

The read path of decision 3 is now live in the browser: the name marketplace grows a Participants tab alongside its For sale / Expired / Sold tabs, so the same pane that browses aliases and domains also browses the opted-in participant pool.

The tab is a plain read, riding the same account-api pattern as the listings and sales tabs (loaded lazily the first time it is opened):

  • The pane requests GET /v1/chain/participants?tags=… on the authenticated account-api /v1/chain surface with the session JWT — the index is global, the JWT only authenticates the request.
  • account-api proxies that read to mail-grpc’s ListParticipants RPC, which runs the trustless on-chain beacon scan (getProgramAccounts + a memcmp filter over the fixed-offset tag bitmap — the layout rule from decision 3). A beacon must carry every filtered tag bit to match.

Each row shows the participant’s wallet, its set tag bits, and whether it published an off-chain detail document (the sealed detail CID of decision 2). Filtering is by a comma-separated list of tag bit positions (e.g. 0,10), applied with Apply.

Known limitation — tags render as raw bit positions. The browser clients have no shared tag-name vocabulary yet, so both the filter input and each row’s tags show the numeric bit positions of the on-chain bitmap, not human labels. The names live only in the mail_model TAG_* constants (program reference); wiring a JS-side name map onto those positions is deferred. The group-offer (quote/send) authoring flow of decision 4 also stays CLI-first for now (item 44) — the tab is a browse-and-discover surface, not yet an authoring one.