Anyone can become a verifier by publishing a kind 15063 replaceable
event whose Markdown content describes how they vet campaigns. The
statement is surfaced prominently in the profile overview so donors
can judge whether to trust the account's judgement.
- Document kind 15063 in NIP.md
- useVerifierStatement / useSetVerifierStatement hooks (read-modify-write)
- /settings/verifier form page with live preview, publish/withdraw
- ProfileVerifierSection rendered first in the profile overview
- Localize all strings across every locale
Verification is gated by the existing campaign moderator pack
(useCampaignModerators / CAMPAIGN_MODERATORS), not a separate allowlist.
- Remove the config.labelers field (AppContext interface, Zod schema,
App.tsx and TestApp defaults) and delete useCampaignLabelers.
- useCampaignVerifications now reads/writes agora.verified labels gated
on the moderator pack (isModerator), same authors filter as the other
label streams.
- Move the verify / remove-verification row INSIDE the moderation kebab's
'Moderator actions' section (leadingExtra slot of ModerationItemsShell),
no longer a top-level item above the section label.
- Revert the isMod || isLabeler widening in ModerationMenu/Overlay back to
plain isMod.
- Remove the trailing 'Verified' checkmark text from 'Remove my
verification'.
- Rename labeler->moderator in agoraVerification, the badge component, and
all locale strings; drop now-unused notVerified / verifiedState keys.
- Update NIP.md to document verification as a moderator action.
Trusted labelers (AppConfig.labelers) can vouch for campaigns via NIP-32
kind 1985 labels in the new agora.verified namespace. Verifier avatars
render as a stacked badge on campaign cards; hovering/clicking opens a
popover listing verifiers (linking to their profiles). Logged-in labelers
get verify / remove-verification controls — verify publishes a label,
unverify issues a kind 5 deletion of their own label.
The read query filters by authors: labelers so verifications from outside
the allowlist are never honored. Kind 1985 label reqs are routed to the
search relays (relay.ditto.pub, relay.dreamith.to) in NostrProvider.
Drop the optional `deadline` tag from kind 33863 campaigns. Removes the
date input and validation from the create/edit form, the deadline chips
on the card, detail, and inline-preview surfaces, and the derived
"ended" state that disabled donations after the deadline. Cleans up the
associated locale keys and NIP.md documentation.
Now that moderators can directly order the Featured row, the second
"Community Campaigns" bucket (approved + not-featured + not-hidden)
is redundant. This commit removes the approval axis end-to-end and
collapses the home page to a single curated section.
Protocol (NIP.md):
- `ModerationLabel` shrinks from six values to four — `hidden`,
`unhidden`, `featured`, `unfeatured`. The legacy `approved` /
`unapproved` labels are now ignored on read and MUST NOT be
published.
- `ModerationAxis` shrinks from three to two: `hide` and `featured`,
both supported by all three surfaces (campaigns, organizations,
pledges).
- The rank tag now only applies to `featured` labels.
- A migration note in NIP.md explains the retirement and tells
clients to ignore lingering approval-axis labels in relay
archives.
UI:
- CampaignsPage drops the Community Campaigns and Pending sections.
Home is now Featured (with the empty state in place when nothing
is featured) → Browse-all link → moderator-only Hidden section.
The labeled-coord targeted fetch shrinks to hidden coords only.
- ModerationMenu loses the Approve / Unapprove rows and the
`hasApproval` / `isApproved` plumbing.
- `CampaignCard`'s `axes` prop drops `'approval'`.
- `ReorderAxis` collapses to a single axis — the type and the
parameter are removed from the reorder hook, provider, context,
and grid component since every reorder targets the featured axis.
- Pledge and organization moderation hooks lose their defensive
`'approved' | 'unapproved'` rejection branches now that those
values are off the `ModerationLabel` union.
i18n (16 locales):
- Five moderation.menu keys removed: `approve`, `unapprove`,
`approvedState`, `toastApproved`, `toastUnapproved`.
- Five campaigns.home keys removed: `community`, `communityDesc`,
`pending`, `pendingDesc`, `pendingEmpty`.
- `campaigns.home.yourCampaignsDesc` rewritten across every locale
to drop the "appears on the homepage once a moderator approves"
copy; new copy points authors at /campaigns for discovery and
notes that the team curates a featured selection on the home page.
Test suite green: tsc, eslint, vitest, vite build all pass.
The first reorder implementation encoded list position directly in
the moderation label's `created_at` and republished the same axis
label with a chosen timestamp. That fights the fold's
newest-event-per-(coord,axis) rule the moment a moderator tries to
lower a campaign's position: the new label has an older
`created_at` than the existing one and the fold rejects it. The
relay accepts the publish, but every subsequent read folds back to
the higher-`created_at` predecessor and the move appears to revert.
Move-up worked (its new `created_at` was strictly newer); move-down,
drag-down, and any drag-to-midpoint that landed below an existing
neighbor silently no-op'd. Anything dragged into the middle of an
already-old list also picked a past timestamp that some relays
reject for being too far behind "now".
The fix decouples sort key from event recency:
- Reorder publishes always use `created_at = now`, so the fold's
newest-wins rule always picks them up.
- The chosen position is encoded as a `["rank", "<integer>"]`
tag on the label.
- `foldModerationLabels` extracts the rank with a `created_at`
fallback, so labels published before this change (and any normal
approve / hide / feature actions that don't carry a rank) still
sort by `created_at` exactly as they used to.
Ranks are sourced from `Date.now() * 1000` (microseconds since
epoch), so:
- Fresh "feature" / "approve" publishes always sit above legacy
labels whose effective rank is a seconds-since-epoch value.
- Midpoint inserts have ~1000x headroom per second of inter-rank
gap, comfortably enough for thousands of reorders before any
renumbering would matter.
- Headroom against `Number.MAX_SAFE_INTEGER` is ~150 years.
Callers downstream (CampaignsPage, CampaignsDiscoverySection,
PledgesDiscoverySection, useFeaturedOrganizations) still consume
`featuredOrder` / `approvedOrder` as `Map<coord, number>` sorted
descending — the map names and shapes are unchanged, only the
value computation is now "rank ?? created_at" instead of
"created_at".
NIP.md updated to document the rank tag, the fallback semantics,
and the reorder operations in terms of ranks.
The Featured row already sorted by the moderator's `featured` label
`created_at`, but reordering required clicking Unfeature then Feature
again — clumsy, and the Community grid sorted only by campaign
`created_at` with no moderator input at all.
This commit promotes the existing axis-label `created_at` into a
first-class sort key on both lists and adds drag-and-drop + kebab-row
UI for moderators.
Protocol (no schema change):
- The Featured row sorts by the `featured` label's `created_at`,
newest first (existing behavior).
- The Community grid now sorts by the `approved` label's
`created_at`, newest first (mirroring the Featured row).
- Reordering = republishing the same axis label for the moved
campaign with a chosen `created_at`. Move-to-top stamps `now`;
move-up stamps `neighborAbove.t + 1`; move-down stamps
`neighborBelow.t - 1`. Drag-to-position picks a value between the
two new neighbors.
- No new tags, no new kinds, no new authority — readers that already
understand the moderation namespace pick up the order for free.
- Conflict model unchanged: newest label per (coord, axis) wins.
Implementation:
- `foldModerationLabels` now populates `approvedOrder` alongside
`featuredOrder`.
- `useCampaignModeration().moderate` accepts an optional explicit
`created_at` for the label event (omitted for normal
approve/hide/feature; passed by the reorder hook).
- New `useReorderCampaign` hook with `moveToTop`, `moveUp`,
`moveDown`, and a general `moveTo(toIndex)` used by drag-and-drop.
- New `ReorderableCampaignGrid` wraps a list of `CampaignCard`s:
- non-mods get a plain grid, zero overhead;
- mods on desktop get HTML5 drag-and-drop with a six-dot handle
on hover (the handle is the only `draggable` element so card
clicks still navigate the underlying `<Link>`);
- mods on mobile get Move up / Move down / Move to top rows
injected into the existing moderator kebab via a context
provider (`ReorderProvider` / `useReorderControlsFor`).
- An optimistic local order smooths the gap between publish and
refetch so the card snaps into the new position immediately; it
rolls back automatically on publish failure.
- Translations added in all 15 non-English locales.
- NIP.md documents the ordering convention in a new
"Moderator-driven Ordering" section under the campaign-moderation
surfacing rules.
/campaigns was a redirect to / (the curated home), and the actual
all-campaigns directory lived at /campaigns/all. Flip the routing
so /campaigns IS the directory, the home page stays at /, and
/campaigns/all becomes a redirect to /campaigns for any external
links and bookmarks that still point there.
Rewrite every internal link/navigate target accordingly (TopNav,
the Browse-all CTA on the home page, the OnboardingGate donor
redirect, NoteCard's kind-33863 nounRoute) and refresh the
doc/comment references in NIP.md and the discovery hooks.
The moderation menu / overlay / review-queue refactor wired pledges
into the same shared moderation components as campaigns and groups,
and three call sites (ActionsPage, ModerationMenu, ModerationOverlay)
imported usePledgeModeration from @/hooks/usePledgeModeration. The
hook file itself was never staged, so the tree didn't build and any
fresh clone would have failed tsc at those imports.
Add the hook (two-axis model — hide + featured, no approval gate, same
agora.moderation namespace and Team Soapbox moderator pack as
campaigns/organizations) and document the kind 36639 surface in
NIP.md alongside the existing 33863 / 34550 entries so the spec
matches the implementation.
Regression-of: c61e9a06
The kind 33863 Querying section previously told clients to aggregate
verified kind 8333 receipts for the campaign total. That undercounts
the campaign whenever a donor pays the BIP-21 QR with a native wallet
(no Nostr receipt published) — which the spec itself documents as a
supported donation path.
Document the actual implementation: the headline 'raised' amount is
cumulative chain_stats.funded_txo_sum on the on-chain `w` address,
fetched from an Esplora endpoint (default mempool.space). Kind 8333
receipts remain the source of the recent-donation list (donor pubkey,
comment, timestamp) but no longer drive the headline total.
Update the Wallet Modes UI matrix to match.
The HD wallet seed is now BIP-39-compatible. Pipeline:
entropy = HKDF-SHA256(nsec, info="agora/v1", length=32)
mnemonic = BIP-39 encoding of (entropy || checksum) // 24 words
seed = PBKDF2-HMAC-SHA512(mnemonic, salt="mnemonic", iters=2048)
The 24 words import cleanly into Sparrow, Electrum, Trezor, Ledger,
BlueWallet, Phoenix, etc., at the BIP-86 / BIP-352 paths. HKDF domain
separation means a leaked mnemonic compromises only the wallet, not
the Nostr identity (unlike the raw nsec).
v1 derivation (nsec used directly as BIP-32 master seed) is retained
as migration-only code. A new /wallet/migrate-v1 page detects funds
at the legacy addresses and builds a single sweep PSBT to consolidate
them into the v2 wallet. A persistent banner on /wallet surfaces the
flow when v1 funds exist.
The mnemonic shows up in two places: a "Back up wallet" dialog on
/wallet, and a section in Profile -> Advanced next to the nsec
backup. nsec backup copy updated to explain the relationship.
Locked test vectors pin the entire derivation pipeline (nsec -> 24
words -> first BIP-86 address -> sp1q...) so any future drift fails
loudly. Regenerate via scripts/derive_vectors.mjs.
Other changes:
- Re-key SP storage NIP-78 d-tag to /v2 so v1 and v2 UTXOs do not mix
- Re-key the persisted receive-address cursor to :v2: namespace
- Relax SP spend-key helper to 16-64 byte seeds (BIP-32 range) so the
migration sweep can sign with the legacy 32-byte v1 seed too
- Remove stale NIP-SP references from derivation comments (the draft
was not relevant to our use case)
- Document the wallet derivation scheme in NIP.md
- Translate every new string to all 10 non-English locales
A campaign may now declare up to two `w` tags — at most one mainnet
on-chain address (bc1q…/bc1p…) and at most one silent-payment code
(sp1…) — and the QR/payment panel combines them into a single BIP-21
URI (`bitcoin:<bc1>?sp=<sp1>`) when both are present. BIP-352-aware
wallets pick the SP parameter automatically; legacy wallets fall back
to the on-chain address.
The campaign form is reorganized around the dual-endpoint model. Users
with nsec access see two avatar chips — "My wallet" and "My private
wallet" — both selected by default and an "Add another address"
disclosure that reveals separate bc1 and sp1 inputs. A typed value
wins over the corresponding chip's HD-derived value, so a cold-storage
address can be substituted without giving up the SP code. Users
without nsec access (extension / bunker logins) see the two custom
inputs unconditionally. At least one of the four sources must resolve.
The on-chain receive-index cursor is still advanced only at publish
time, and now only when "My wallet" is selected AND no custom
on-chain value was provided — so the cursor never burns on a no-op
edit or on a publish where the user overrode the chip with their own
address.
`ParsedCampaign.wallet` is replaced by `ParsedCampaign.wallets`, a
`{ onchain?, sp? }` struct. Consumers (`useCampaignDonations`,
`useDonateCampaign`, `useProfileCampaignStats`, `useOnchainZaps`,
`CampaignCard`, `CampaignDetailPage`, profile rails) keep their
existing on-chain semantics by reading `wallets.onchain`. The
"Private campaign" badge and hidden-aggregates UI now trigger on
SP-only campaigns (no on-chain endpoint), matching the spec.
Organizations now use a two-axis moderation model — featured and hidden.
The approval axis is campaign-only. Every Agora-tagged organization is
publicly visible by default; moderators curate by lifting orgs into the
Featured shelf or suppressing them with a Hidden label, nothing in
between.
Concretely:
- CommunityModerationMenu drops the Approve / Unapprove items. The org
side never publishes those labels; useOrganizationModeration's
mutate() rejects them defensively so a stray UI bug can't poison the
label stream with axis-mixed events. The shared ModerationData type
still tracks approvedCoords for symmetry with the campaign side, but
the org UI never reads or writes it.
- /communities loses the in-flight 'Approved organizations' grid that
was never finished and was conceptually wrong here. The moderator-only
rail formerly called 'Pending review' is renamed 'Needs review' and
re-derived as t:agora AND not featured AND not hidden.
Two perf fixes that should make Featured paint noticeably faster:
- The 200-event discovery query and the 2000-event label fold that
power the moderator rails now live INSIDE ModeratorReviewSections.
Non-moderator viewers (the overwhelming majority on /communities)
never trigger either query — they used to fire at the page level
whether they were used or not.
- Every CommunityMiniCard used to subscribe to useOrganizationModeration
individually so it could overlay the Hidden badge and kebab. With
18+ cards per page, that meant 18 component subscriptions to a
query no non-mod cared about. The badge + kebab now live inside a
single CommunityModerationOverlay that's gated on isMod up front
and returns null for everyone else; non-mods never subscribe.
- staleTime on useOrganizationModeration and useFeaturedOrganizations
bumped from 30s to 5m, with a 1-hour gcTime. Moderators feature and
hide on human timescales, not seconds — repeat visits to /communities
shouldn't pay a fresh relay round-trip every half-minute. Local
changes still invalidate the keys explicitly so moderator actions
show up immediately.
Also reverts two pieces of unrelated scope creep I built by mistake:
the 'Approved campaigns' section on the org detail page and the
moderator kebab on the campaign detail page. Those weren't asked for —
they were chasing a misread of an earlier instruction.
NIP.md updated to reflect the campaign-vs-org axis split and the new
'Needs review' surface name.
Replace the hardcoded FEATURED_ORGANIZATION_AUTHORS allowlist with the
same NIP-32 label flow that curates featured campaigns: Team Soapbox
pack members publish kind 1985 labels in the agora.moderation namespace
tagging an organization's 34550:<pubkey>:<d> coordinate as featured,
hidden, or approved, and useFeaturedOrganizations folds those labels
into the /communities Featured shelf.
The campaign and organization label streams share a single namespace
and a single moderator pack — they're separated purely by which kind
prefix the 'a' tag carries. To keep that contract enforced in one
place, the constants, types, and folding logic are now in
src/lib/agoraModeration.ts; useCampaignModeration and the new
useOrganizationModeration both call foldModerationLabels with their
respective kind. The campaign hook's external surface
(AGORA_MODERATION_NAMESPACE, ModerationLabel, CampaignModerationData)
is preserved via re-exports so existing call sites don't move.
Moderators see a CommunityModerationMenu kebab overlaid on every
CommunityMiniCard exposing approve/unapprove, hide/unhide, and
feature/unfeature. Mounting reads moderation state once per page from
the shared TanStack cache, mirroring CampaignCard. Non-moderators see
no overlay (the menu returns null) and no card affordances change.
The 'My organizations' shelf intentionally ignores moderation — a
user's own founded, moderated, or followed organizations always render
regardless of label state. Only the Featured shelf consumes the
curation rollup.
The Featured grid is uncapped: moderators control how many orgs
surface by labeling, and ordering follows the recency of each
'featured' label so re-publishing bumps an org to the top.
NIP.md's 'Campaign Moderation Labels' section is renamed to 'Agora
Moderation Labels' and documents the kind-34550 coord form and the
'My organizations ignores moderation' rule.
Note: existing surfaced organizations will disappear from the shelf
until a moderator publishes featured labels for them.
The Agora activity feed now filters strictly to Agora-created content via
the relay-indexed single-letter `t:agora` tag. Multi-letter tags like
NIP-89 `client` are not indexed by relays and cannot serve this purpose.
Every event Agora publishes that represents first-class Agora content
now carries `["t", "agora"]`, added via a new `withAgoraTag` helper
in `src/lib/agoraNoteTags.ts` that dedupes against any user-supplied
`t:agora` tag.
Tagged at publish time:
- Communities (kind 34550) — CreateCommunityPage
- Campaigns (kind 30223) — CreateCampaignPage, useArchiveCampaign
- Pledges (kind 36639) — CreateActionPage (alongside agora-action)
- Calendar events (kinds 31922 / 31923) — CreateEventPage and
CreateCommunityEventDialog
- Onchain zaps (kind 8333) — useOnchainZap, useDonateCampaign,
SendBitcoinDialog
- Zap goals (kind 9041) — CreateGoalDialog
- NIP-22 comments (kind 1111) — usePostComment, covering every comment
authored from within the app regardless of root kind
- Kind 1 notes — already covered by ComposeBox default tags
Intentionally not tagged: reactions, reposts, follows, profile metadata,
lists, settings, badges, vanish requests, encrypted DMs, live chat.
useAgoraFeed tightened:
- Entity kinds and Agora-comment kinds now require `#t=agora` at the
relay layer (server-side filter).
- World layer (kind 1111 / 1068 with `#k=iso3166|geo`) remains
unfiltered — intentionally cross-client.
- `#Agora`-tagged kind 1 notes still surface from any author (preserves
viral / opt-in discovery via user-typed hashtags).
- Donation enrichment now requires the Agora marker on zap receipts.
- `isRelevantAgoraEvent` rewritten as a strict checker that demands
the marker for everything outside the world layer.
Legacy content without the marker disappears from the feed. It remains
reachable by direct link and via kind-specific directories (e.g.
`/campaigns/all`). Authors who edit a legacy event through the Agora UI
will automatically add the marker via the helper.
NIP.md updated with a new "Agora Content Marker" section under "Agora
Protocols" — documents the tagged-kind table, the untagged-kind list,
the canonical query shape, and the backward-compatibility behavior.
Replaces the kind 30223 Campaign spec with a clean kind 33863 design.
Hard cutoff: no migration, no dual-read, no legacy support.
Key changes:
- Kind number 33863 (FUND on T9 keypad).
- Single `w` tag carries one bech32(m) wallet endpoint. Prefix
selects the mode: `bc1q`/`bc1p` for public on-chain, `sp1` for
silent payments (BIP-352). Other prefixes are rejected.
- Recipient `p` tags removed. Campaigns are self-authored; the
event author is the beneficiary and owns the wallet in `w`.
- No split weights, no dust calculations, no BIP-340/341 Taproot
derivation from Nostr pubkeys.
- `t` topic tags and `agora.category` labels removed. Discovery is
via search, country (`#i`), and moderator curation.
- `image` -> `banner`, with required NIP-92 `imeta` for dim,
blurhash, MIME, and SHA-256.
- `goal` is a single integer USD value, no unit, no currency code.
- `status` tag removed. To close a campaign, publish a NIP-09 kind
5 deletion referencing the campaign's `a` coordinate.
- `location` legacy tag removed.
- Kind 8333 receipts for campaigns carry no `p` tags; verification
matches tx outputs against the campaign's `w` address. SP
campaigns publish no receipts and hide all aggregate UI by design.
Remove the Discover page and route entirely. In the top nav, swap the
Discover entry for a Support link pointing at /campaigns/all (the all
campaigns directory).
Also drops the now-orphaned DiscoverHero component and useDiscoverFeed
hook, and updates the NIP.md campaign moderation note that referenced
the deleted /discover route.
Pledges are now open-ended by default — drop the optional start
date input and stop publishing the legacy `start` tag. The kind
36639 `created_at` already marks when the pledge becomes active.
Hide the built-in cover image templates so the upload area is the
only path for cover art, encouraging pledgers to supply their own
visuals. The thumbnail strip remains available via the
`templates` prop on `CoverImageField` for other surfaces.
Rename the description heading from "Story" to "Description"
to avoid implying a narrative expectation, and label the optional
tag input as "Recommended" to nudge categorization.
Mirror the same changes in `CreateActionDialog` (the community
scoped variant) and mark `start` as legacy in `NIP.md`.
Featured was a hardcoded array of naddrs in src/lib/featuredCampaigns.ts.
Promote it to a third moderation axis (`featured` / `unfeatured`)
alongside `approved` and `hidden`, managed by Team Soapbox pack members
through the same kebab menu on each campaign card.
The Featured row on the homepage now:
- Reads from `moderation.featuredCoords`, ordered newest-featured-label first.
- Caps at 4 campaigns.
- Adapts its grid to 1/2/3/4 desktop columns based on count (mobile stays
one column), collapses when nothing is featured, and surfaces the hero
`variant="featured"` card only when exactly one campaign is featured.
- Hide still wins: a featured-then-hidden campaign disappears from the row.
Rename the homepage's second section from "All campaigns" to "Community
Campaigns", which more accurately reflects that it's the approved-not-
hidden set with featured campaigns deduplicated out.
Add a new /campaigns/all page that lists every campaign found on relays
(approved, pending, and unmoderated alike), with a "Show hidden" toggle
that adds hidden campaigns back in. The Discover page's "All campaigns"
link now points here instead of /, since the homepage is no longer a
truly-all view post-moderation. Also surface a small "Browse all
campaigns" link beneath the Community Campaigns grid so the new page is
discoverable from home.
Update NIP.md to document the third axis and the home-page surfacing
rules (Featured row, Community Campaigns grid, Discover shelf).
Delete src/lib/featuredCampaigns.ts entirely — there's no longer a build-
time list to maintain.
Move campaign curation from a hardcoded HIDDEN_CAMPAIGN_COORDS set to a
real moderation system. Team Soapbox (kind 39089 follow pack
k4p5w0n22suf) is the moderator roster; each member signs NIP-32 kind
1985 labels in the agora.moderation namespace to approve or hide a
campaign. The home page and Discore shelf render the approved-and-
not-hidden set; moderators additionally see Pending + Hidden sections
and a per-card kebab menu. Non-moderator authors get a Your Campaigns
section explaining their campaign is live on Nostr but awaiting a
homepage approval.
A single Bitcoin transaction with N outputs now produces a single kind
8333 onchain-zap event listing every recipient under its own `p` tag,
instead of one event per recipient. The `amount` tag carries the total
sats paid to the listed recipients (the full donation, excluding the
donor's change).
This is straight-forward forward-compatibility: legacy single-recipient
events are just the degenerate case (one `p` tag, amount equal to the
one recipient's slice). Aggregators (`useCampaignDonations`,
`useGlobalDonations`) simplify to summing the `amount` tag across every
matching event — under both schemas an event's `amount` is the total
paid to the recipients listed in that event, so the sum across all
events for a campaign is the campaign total either way.
The verifier (`verifyOnchainZap`) now sums tx outputs paying any listed
recipient's derived Taproot address and strips the sender from the
recipient set so a tx that includes the sender plus legitimate
recipients still verifies. The notifications surface uses a new
`getZapAmountSatsForRecipient` helper to attribute only the viewer's
estimated slice (amount / p_count) rather than crediting them with the
full multi-recipient donation. `CampaignDetailPage` keeps its
group-by-(txid, donor) reply rendering so legacy multi-event donations
still collapse to a single donation card.
Authors can soft-close a campaign by republishing it with a
`["status", "archived"]` tag. Archived campaigns are hidden from the
main fundraisers feed and the donate button is disabled, but the detail
page still loads by direct link so existing donors can find it and
past donations remain attached. The author sees Archive / Reopen
buttons on the detail page and an Archived badge on cards.
useCampaigns gains an `includeArchived` option (default false) so a
future profile view can opt in. NIP.md documents the new status tag.
Pivot the homepage from a Twitter-style social feed to a GoFundMe-style
fundraising hub. Introduces a new addressable kind 30223 "Campaign" that
carries the marketing-style metadata (title, summary, cover image, story,
category, goal, deadline, location) plus a list of recipient pubkeys with
optional split weights. Documented in NIP.md alongside the kind 8333
onchain-zap spec it builds on.
Donations are sent as a single multi-output Bitcoin transaction (one
output per recipient, derived Taproot addresses) using the existing
buildUnsignedMultiOutputPsbt + useBitcoinSigner infrastructure that
backs community on-chain zaps. After broadcast, the client publishes
one kind 8333 receipt per recipient with the campaign's `a` coordinate
so the donation aggregates into the campaign's totals.
UI surfaces:
- /campaigns is now the default homePage. Hero, two featured slots
(placeholders in src/lib/featuredCampaigns.ts), then a grid of
user-submitted campaigns.
- /campaigns/new is a full create form with cover upload, slug
collision check, recipient builder with per-row weights, and
preset/custom donation-amount UX.
- naddr1 identifiers for kind 30223 route to CampaignDetailPage via
NIP19Page (full story rendered through the existing ArticleContent
markdown component, plus a sticky donate rail with progress).
- DonateDialog presets are tuned for on-chain amounts (10K-1M sats)
with a dust-aware minimum guard derived from the split math.
- Fundraisers sidebar item with a HandHeart icon.
Kept the existing social-feed pages addressable from the sidebar; the
overhaul is scoped to the home/landing experience rather than removing
the underlying Nostr features.
The @samthomson/nostr-messaging library opens fresh NRelay1 sockets per
participant per relay outside the shared NPool, fanning out to every
conversation partner's NIP-65 + NIP-17 inbox relays plus all
discoveryRelays in hybrid mode. In practice this drives connection counts
to several hundred relays per session.
Rather than band-aid the fan-out, drop the feature entirely and point
users to White Noise for end-to-end encrypted Nostr chat.
- Replace /messages with a 'Install White Noise' CTA card (route kept)
- Delete MessagingSettingsPage, DMProviderWrapper, messaging-intro.png
- Remove DMProvider wrapper and PROTOCOL_MODE config from App.tsx
- Drop messaging config from AppConfig, AppConfigSchema,
EncryptedSettingsSchema, EncryptedSettings, and the NostrSync /
useInitialSync sync paths
- Remove messages sidebar entry, default sidebarOrder slot, and
SettingsPage messaging card
- Uninstall @samthomson/nostr-messaging and drop its tailwind content
glob and vitest deps.inline entry
- Update copy in PrivacyPolicy, AdvancedSettings delete-account warning,
ProfileSettings nsec warning, RequestToVanishDialog deletion checklist,
MainLayout comment, and NIP.md
- Leave kind 4 rendering (EncryptedMessageContent) intact so DM events
authored elsewhere still display in feeds and quote embeds
Extract isAuthorizedAward helper as the single source of truth for
membership award validation, used by both resolveMembership and
useMyCommunities. Simplify resolveCommunityModeration by dropping
the dead banned-reporter guard from pass 1 (impossible under strict
rank ordering). Flip useMembersOnlyFilter default to opt-in to match
the spec's MAY wording, and reword the NIP to match.
Bookmarking a kind 34550 community now writes to the NIP-51 Communities
list (kind 10004) keyed by the addressable coordinate, so the reference
stays valid across community updates. My Communities merges bookmarked
communities as a third discovery source alongside founded and member-of,
with Founder/Member/Bookmarked badges on each card.
Bookmark toasts live on the mutation itself so they survive the more-menu
dialog unmounting between .mutate() and publish resolution.
- Gate social actions by projected stat thresholds (< 70) so visitors
can only help stats in visual distress
- Add energy category with Energy Drink and Power Nap Pillow items
- Apply 6-hour recency window to interaction queries (limit 30)
- Fix BlobbiActionsProvider tree placement so BlobbiPage shares context
with the companion layer
- Preserve event content in dev editor (don't overwrite checkpoint JSON)
- Show Needs Now summary in activity tab with priority badges
- Remove unused need-driven consolidation infrastructure
Regression-of: 9aecefff
Implement zap goals (kind 9041) linked to communities via a-tag.
Includes goal creation dialog, progress tracking from zap receipts,
recipient profile/lightning address display, community link, and
members-only filtering. Goals appear in community detail Fundraising
tab, activity feed, and main feed via NoteCard.
A Birdex is a replaceable per-author index of every bird species the
author has ever confirmed via kind 2473, stored as positional i/n tag
pairs in chronological first-seen order. In feeds, show a compact
tile strip of the most recently-added species with a "+N" capstone
when the list overflows — mirroring how kind 3 follow lists preview
members as an avatar stack plus "+N more". On the post-detail page,
render every species as a responsive grid so visitors can browse the
author's whole life list.
Each tile resolves the species' Wikidata entity through English
Wikipedia to pull a thumbnail and common-name label, reusing the
same fetch path as kind 2473 detection cards. The Wikidata URL is
sanitized before being routed, and the paired n tag provides a
scientific-name fallback while the remote lookup is in flight.