# Bitsocial > Bitsocial is an open-source peer-to-peer social network and app ecosystem. Bitsocial.net is the public web surface for the network overview, project catalog, privacy notice, and technical documentation. This file expands `https://bitsocial.net/llms.txt` into a large inline corpus. It includes public-site routing notes, the full English docs corpus, and companion project READMEs for the main Bitsocial repos linked from the project directory. ## How to use this file Use this file when you want a single context bundle for Bitsocial instead of following separate doc links. Two interpretation notes matter: - Bitsocial is not a centralized social platform. The core model is peer-to-peer publishing, key-controlled ownership, and app-level choice instead of one company-controlled backend. - Some routes exist in the React app for development (`/about` and `/blog`) but are not part of the current public production site. Do not treat them as canonical public content. ## Public site surfaces - [Home](https://bitsocial.net/): Public overview of Bitsocial, its decentralized positioning, and the main paths for trying apps or reading docs. - [Projects](https://bitsocial.net/projects): Catalog page grouping public clients, anti-spam modules, and operator tools. - [Privacy](https://bitsocial.net/privacy): Privacy notice for the about site, docs, analytics, and newsletter handling. - [Docs home](https://docs.bitsocial.net/): Technical docs covering protocol notes, roadmap material, app notes, infrastructure docs, and contributor playbooks. - [Docs llms.txt](https://docs.bitsocial.net/llms.txt): Curated docs-specific routing index. - [Docs llms-full.txt](https://docs.bitsocial.net/llms-full.txt): Docs-scoped full inline corpus. - [Bitsocial Chain](https://chain.bitsocial.net/): BSO token and proposed Ethereum L2 appchain landing page. - [Bitsocial Chain llms.txt](https://chain.bitsocial.net/llms.txt): Chain-specific routing index. - [Bitsocial Chain llms-full.txt](https://chain.bitsocial.net/llms-full.txt): Chain-focused inline context. - [Stats dashboard](https://stats.bitsocial.net/): Grafana-backed public stats surface for Bitsocial apps and infrastructure. ## Landing page highlights - [Home](https://bitsocial.net/): Bitsocial is an open-source peer-to-peer network for social apps, with no servers, no global bans, where users and communities are cryptographic property. - [The problem](https://bitsocial.net/#problem): It's not a conspiracy, it's an architecture. Every mainstream social platform is a private database on private servers, and livelihoods, businesses, and communities now live on it, so decisions made inside a handful of companies reach directly into the physical world. That design has three consequences, no matter who runs it. - [Core features](https://bitsocial.net/#core-features): Bitsocial is open source, peer-to-peer via IPFS/libp2p, app-oriented, serverless by default, locally moderated with no protocol-level global bans, and built around key-controlled identities and communities. - [Browser P2P](https://bitsocial.net/#browser-peer): Open a Bitsocial web app in a normal tab and it joins the network itself, fetching posts from other peers and publishing your own. That's the hard part — and why every other decentralized social network still keeps a server in the middle. - [Sanctuary comparison](https://bitsocial.net/#decentralized): Compares Server-based (Bluesky, Mastodon, Lemmy); Blockchain-based (Farcaster, Lens, DeSo, Steemit); Bitsocial (Pure P2P with arbitrary anti-spam challenges) across self-hosting cost, who keeps content online, scaling, custom anti-spam logic, and takedown choke points. - [Deep comparison tables](https://bitsocial.net/#nostr-comparison): Sourced modal tables compare Bitsocial with Nostr, Bluesky, ActivityPub / Fediverse, Farcaster, Lens, DeSo, Steemit across Spam replies, Community rules, Scaling costs, Data hosting, Censorship points, Who accepts posts?, First post, What your browser is, Human names, Finding content. - [Arbitrary Challenges](https://bitsocial.net/#arbitrary-challenges): Each node runs its own anti-spam challenge, exchanged purely peer-to-peer with the user. When spam evolves, nodes adapt without changing the protocol. Example modules include any code, AI-powered moderation, SMS OTP, whatever comes next. - [Text-only Protocol](https://bitsocial.net/#text-only-protocol): The protocol carries only text, and it's not a hidden network. There's no global admin and no server to seize, so nobody can ban you from the whole network. Moderation just moves closer to you. - [Adoption thesis](https://bitsocial.net/#adoption-thesis): It can't be censored, it never goes down, and you keep everything you earn. Here's who shows up — and why. - [Master Plan](https://bitsocial.net/#master-plan): Bitsocial wins by attracting as many builders as possible. Run your own unstoppable community, develop your own decentralized social app, or even launch your own business on top of Bitsocial. The tide rises with every builder. - [Newsletter](https://bitsocial.net/#mailing-list): Get notified about protocol milestones, new Bitsocial apps, and ecosystem updates. Low frequency, high signal. - [FAQ](https://bitsocial.net/#faq): Every section of this site answers one. Click a question to jump to it. ## Public apps and tools - [5chan](https://bitsocial.net/apps/5chan): Decentralized imageboards with direct web, APK, and desktop downloads. - [Seedit](https://bitsocial.net/apps/seedit): Forum-style Bitsocial client with web, APK, and desktop builds. - [bitbones](https://bitsocial.net/apps/bitbones): Bare-bones reference client where every view is a thin wrapper over one hook. - [HuggingSocial](https://bitsocial.net/apps/huggingsocial): Peer-to-peer hub for finding, discussing, and downloading open AI models. - [Mintpass](https://bitsocial.net/apps/mintpass): NFT-backed access control for communities that need stronger anti-spam gates. - [AI Moderation Challenge](https://bitsocial.net/apps/ai-moderation-challenge): OpenAI-compatible moderation checks against each community's rules. - [Spam Blocker](https://bitsocial.net/apps/spam-blocker): Centralized risk scoring layer for filtering abusive publications. - [R9K Challenge](https://bitsocial.net/apps/r9k-challenge): Robot9000-style originality gate for communities that want anti-repost posting UX. - [Wordfilter Challenge](https://bitsocial.net/apps/wordfilter-challenge): Community wordfilters enforced as a posting rule instead of a display filter. - [Captcha Canvas Challenge](https://bitsocial.net/apps/captcha-canvas-challenge): Custom image captchas for communities that want human verification. - [Voucher Challenge](https://bitsocial.net/apps/voucher-challenge): Invite-style voucher codes for communities that prefer controlled growth. - [EVM Contract Call](https://bitsocial.net/apps/evm-contract-call): On-chain gating for communities that want token or contract checks. - [Flags Challenge](https://bitsocial.net/apps/flags-challenge): Verified flag issuer challenge for country and board-specific flair. - [Bitsocial CLI](https://bitsocial.net/apps/bitsocial-cli): Command-line control for nodes, communities, and automation workflows. - [Bitsocial Seeder](https://bitsocial.net/apps/bitsocial-seeder): Public seeder for Bitsocial communities, packaged for Docker and npm. - [Pubsub Provider](https://bitsocial.net/apps/pubsub-provider): Fallback pubsub relay and routing provider for Bitsocial operators. - [Bitsocial Indexer](https://bitsocial.net/apps/bitsocial-indexer): Self-hostable crawler, search index, and web UI for Bitsocial communities. - [Bitsocial Previewer](https://bitsocial.net/apps/bitsocial-previewer): Link-preview and redirect server that renders share cards for Bitsocial posts. - [Challenge Composer](https://bitsocial.net/apps/challenge-composer): Offline-first editor for a community's anti-spam challenge settings. - [Bitsocial Telegram Bots](https://bitsocial.net/apps/telegram-bots): Feed bots that relay new Bitsocial posts into Telegram channels or groups. - [Bitsocial GitHub Alerts](https://bitsocial.net/apps/bitsocial-github-alerts): Telegram bot that posts compact GitHub activity for Bitsocial repositories. - [5chan Board Manager](https://bitsocial.net/apps/5chan-board-manager): Board administration tooling for custom 5chan communities. ## Landing page corpus ### Hero Source: https://bitsocial.net/ Bitsocial is an open-source peer-to-peer network for social apps, with no servers, no global bans, where users and communities are cryptographic property. ### The problem Source: https://bitsocial.net/#problem The social layer of the web has an owner. It's not a conspiracy, it's an architecture. Every mainstream social platform is a private database on private servers, and livelihoods, businesses, and communities now live on it, so decisions made inside a handful of companies reach directly into the physical world. That design has three consequences, no matter who runs it. - Your identity is rented.: Your account, your audience, and your community exist because a company's database says so. Nothing you built there is yours. Bitsocial's answer: Cryptographic property. - One owner is one point of failure.: A company can be pressured, sold, shut down, or simply change its mind. Everything downstream of it changes with it. Bitsocial's answer: No servers. - One rulebook governs everyone.: A single ruleset, written privately and applied globally, decides what billions of people can say to each other. Bitsocial's answer: No global bans. Founder note: What's the point of creating a company or product to make the world better, if centralized social media apps can just ban you, and then you can no longer reach your customers? — Esteban Abaroa, Bitsocial founder ### Core features - Open Source: Bitsocial is free and open source software: the protocol and clients are public, forkable, and open to outside contributions. Anyone can inspect the code, ship improvements, or build a compatible app without asking permission from a company. - Peer To Peer: Bitsocial is neither federated nor on-chain. Each community is a peer-to-peer swarm using IPFS, closer to BitTorrent than a hosted website: users seed effortlessly, nodes run on cheap hardware, and each community is fully independent and sovereign. A browser tab can join that swarm as a peer, so the web app is not a client of someone else's server. - Social Apps: Anyone can build a Bitsocial app with its own interface, discovery model, or defaults. Apps compete on product quality instead of locking users into a private database, because compatible clients can share the same communities, identities, and network. - No Servers: Bitsocial does not require every community to rent a datacenter box, buy a domain, or manage SSL just to stay online. A community node can run from home on consumer hardware, and there is no single company-run backend that can take the whole network down. - No Global Bans: Moderation still exists, but it stays local. Community owners set rules for their own spaces and apps can choose what they index or show, yet there is no protocol-level super-admin who can erase a profile or seize a community from the network itself. - Cryptographic Property: Profiles and communities are controlled by private keys, not by revocable platform accounts. You can delegate hosting without giving away ownership, so your identity and community behave more like wallet-controlled property than a username rented from a company. ### Browser P2P Source: https://bitsocial.net/#browser-peer The browser is the breakthrough. Open a Bitsocial web app in a normal tab and it joins the network itself, fetching posts from other peers and publishing your own. That's the hard part — and why every other decentralized social network still keeps a server in the middle. - The tab runs a node: The page boots a Helia node and speaks the same protocol as desktop and CLI nodes, reaching peers over the same kind of encrypted connection any website uses. Publishing from a browser became reliable in 2026, once the last upstream fix landed. - Nothing to shut down: The page is only a client: any copy of it, on any domain or as a desktop build, reaches the same network. Your identity and your community are keys you hold, not rows in a company's database, so there is no account to lose and no host to seize. ### Sanctuary comparison Source: https://bitsocial.net/#decentralized No servers to rent. No domains to buy. No chains to sync. A Bitsocial node runs on a Raspberry Pi. And each community can enforce its own anti-spam challenge: captchas, reputation, SMS, payments, tokens, IP checks, or anything else that can be coded. | Topic | Server-based | Blockchain-based | Bitsocial | | --- | --- | --- | --- | | Self-hosting cost | Server + domain + SSL | Expensive node, hub, or RPC | Extremely cheap, runs on Raspberry Pi | | Who keeps it online | Server operators and shared indexers | Chain, hub, or RPC infrastructure | Community owners + helper seeders | | Scaling model | Bigger servers and indexers, higher bills | More state, heavier infra | More peers, more bandwidth | | Custom anti-spam logic | Not built into the protocol | Tied to chains, hubs, or fees | Built in: challenge can be anything | | Takedown choke points | Host, registrar, SSL, DDoS provider | Validators, hubs, RPCs | No single choke point | ### Arbitrary Challenges Source: https://bitsocial.net/#arbitrary-challenges Anti-spam? Future-proof. Each node runs its own anti-spam challenge, exchanged purely peer-to-peer with the user. When spam evolves, nodes adapt without changing the protocol. Plug-in examples: any code, AI-powered moderation, SMS OTP, whatever comes next. Founder note: The only Bitsocial innovation is solving spam and DDoS using arbitrary challenges like captchas over p2p. ### Text-only Protocol Source: https://bitsocial.net/#text-only-protocol Freedom has moderation. The protocol carries only text, and it's not a hidden network. There's no global admin and no server to seize, so nobody can ban you from the whole network. Moderation just moves closer to you. - Communities moderate. Each community sets and enforces its own rules, with its own anti-spam. There's just no single admin above the network who can ban you everywhere. - Apps moderate too. Client developers can hide known-dangerous communities. Developers can run opt-in reputation and scoring services to increase safety. - Not a hidden network. Peer-to-peer is not fully anonymous: like BitTorrent, your IP can be visible to peers. Text-only protocol: images and videos are embedded from external websites. Founder note: No one can force a community to censor you, and no one can force it not to. If a community has bad mods, you can use another one. ### Adoption thesis Source: https://bitsocial.net/#adoption-thesis Owning beats renting. It can't be censored, it never goes down, and you keep everything you earn. Here's who shows up — and why. Reasons to adopt: - Nobody can deplatform you: No protocol super-admin can erase a profile or seize a community. - It never goes down: No company backend or instance can take the whole network down. - You keep 100% of what you earn: Run your own ads and keep every cent. Nobody can demonetize you. Adoption cohorts: - Innovators: Testers, developers, and protocol builders prove the apps while earning first-mover visibility. - Early adopters: Communities arrive when adminless reach lets them keep their audience through the next ban. - Early majority: Creators and operators follow when monetization makes a community worth building around. - Late majority: Mainstream users trust the network once resilience proves it works every day. - Laggards: They join once their people are already there and switching stops feeling risky. ### Master Plan Source: https://bitsocial.net/#master-plan A call to builders. Bitsocial wins by attracting as many builders as possible. Run your own unstoppable community, develop your own decentralized social app, or even launch your own business on top of Bitsocial. The tide rises with every builder. - Phase 1 - Decentralize imageboards and forums: Imageboards are the simplest form of social media to decentralize: anonymous posting, few default boards, and no profile graph. 5chan proves Bitsocial can replace centralized imageboards while removing global admins from the equation. - Phase 2 - Launch Bitsocial Chain: In order to decentralize all social media, Bitsocial apps will need killer features and strong network effects: unstoppable financial structures, decentralized Bitsocial domains (.bso), awards and tipping, common liquidity, and practical monetization. All of this will be powered by Bitsocial Chain, the proposed Ethereum L2 appchain economic layer for Bitsocial apps. - Phase 3 - Decentralize Twitter/X: Phase 3 should launch a focused, decentralized alternative to Twitter/X: posts, replies, reposts, follows, real-time public conversation, and communities that can pull network effects from every Bitsocial client. It should make text-first public conversation work exceptionally well, while keeping RPCs, feeds, instances, algorithms, ads, and ranking replaceable. - Phase 4 - Scale Bitsocial economies: The next layer is ecosystem funding and infrastructure pluralism. Bitsocial Forge's RPC should not be the only successful RPC: many businesses, independent teams, anonymous operators, and community entities should build RPCs, media hosting, discovery, moderation, and other services on top of Bitsocial. Funding should push that decentralization forward, so developers and operators can compete to make the network faster, cheaper, more reliable, and harder to capture. - Phase 5 - Decentralize all social media: With the core apps, public RPCs, profile nodes, discovery, and monetization in place, Bitsocial can fund and build the long tail of social clients: blogging, crowdfunding, creator video and a credible YouTube alternative, niche experiments, and every format too early for the first four phases. Bitsocial Forge can build some of them, centralized clients can compete too, they do not all have to be FOSS, and decentralized community grants can fund as many developers as possible. The end state is not one app. It is a market of clients, nodes, services, and communities that can replace platform ownership with protocol competition. Social media finally finds its equilibrium: a fully decentralized, peer-to-peer social network that nobody owns; Bitsocial. ### Newsletter Source: https://bitsocial.net/#mailing-list Stay in the Loop Get notified about protocol milestones, new Bitsocial apps, and ecosystem updates. Low frequency, high signal. No spam, ever. Unsubscribe anytime. ### FAQ Source: https://bitsocial.net/#faq Common Questions Every section of this site answers one. Click a question to jump to it. - What problem does Bitsocial solve? - What is Bitsocial? How does it work? - Why hasn't someone built this already? - Aren't there decentralized social networks already? - With no servers, what stops spam? - What about illegal content? - Why would people switch to Bitsocial? - What's the plan from here? How can I help? - Who is behind Bitsocial? How was it funded? - How do I follow along? ## Landing page deep comparison tables ### Nostr vs Bitsocial Source: https://bitsocial.net/#nostr-comparison | Topic | Nostr | Bitsocial | | --- | --- | --- | | Spam replies | Spam can reach the thread first. Cleanup usually happens after. | A community can challenge posters first and reject spam before it appears. | | Community rules | Rules depend on the relay or app people happen to use. | Rules belong to the community, so accepted posts pass the same gate. | | Scaling costs | High traffic pushes more server and bandwidth cost onto relays. | More readers can also mean more people helping share the content. | | Data hosting | Relays keep posts online and deliver them to readers. | Users can fetch and share posts directly with each other. | | Censorship points | If major relays stop carrying you, many users may never find your posts. | A community can stay reachable while peers keep sharing it. | | Who accepts posts? | A group depends on relays and apps agreeing which posts count. | The community is the thing you post to; it accepts or rejects posts itself. | | First post | A web client feels simple only while its relay choices work. | You can start with a local key, without making a server own your identity. | | What your browser is | A client of whichever relays it is configured to use. | A peer. It runs a node and fetches from other peers. | | Human names | Your account is a long public key; readable names need extra providers. | Keys stay yours, and .bso names can point to people or communities. | | Finding content | Discovery often follows the relays and search services with the most data. | Discovery finds peers; it does not become the place that owns the content. | Details: - Spam replies: Nostr can show which key wrote a reply, but publishing and moderation are separate. A relay or app can hide junk after it arrives. Bitsocial gives the community a front door: complete the challenge, or the reply is never accepted into the thread. - Community rules: On Nostr, a relay can set rules for that relay, and an app can filter what that app shows. Those rules do not truly belong to the community. Bitsocial lets each community choose the gate for posting, such as account age, reputation, captcha, whitelist, payment, or another challenge. - Scaling costs: In Nostr, relays are still the servers keeping posts online and sending them to readers. A popular post can be great for users while becoming expensive for relay operators. In Bitsocial, readers can also help distribute the content they read, so popularity can add capacity instead of only adding server cost. - Data hosting: Nostr posts are portable because users control their keys, but a server still has to keep the posts online. In normal Nostr use, that job belongs to relays. Bitsocial separates finding people from carrying the data: small lookup services can help find peers, then users fetch and share the content directly. - Censorship points: A Nostr key can survive a relay ban, but visibility still depends on the relays, search services, and apps people actually use. If those places stop carrying you, the account exists but the audience may not see it. Bitsocial removes the relay as the center of power: users can keep sharing a community through different discovery paths as long as peers keep it online. - Who accepts posts?: Nostr can build group features, but the group still depends on relays and apps agreeing on what belongs there. That is fragile for forum-style communities. Bitsocial lets the community accept posts directly, which fits forums, imageboards, and subreddit-style spaces better. - First post: Nostr can feel easy when a good web client has good relays already configured. The hidden dependency remains: those relays must accept, store, and serve the activity. Bitsocial can create the user's key locally and let the app choose direct peer-to-peer or hosted access without making an account server own the identity. - What your browser is: A Nostr web client holds websocket connections to relays, and those relays have to accept, store, and serve the content it reads and writes. Bitsocial browser clients run a Helia node in the page: they ask HTTP routers which peers hold a community, then fetch and publish over libp2p directly. Browser peers dial over secure WebSockets, and the gossipsub sequence-number fix in @libp2p/gossipsub 15.0.21 is what stopped posts published from a tab being discarded by the rest of the network. - Human names: Nostr's base identity is a public key, which is portable but hard to remember. Readable names are useful, but they are added through separate conventions and providers. Bitsocial also uses public keys while adding .bso names, so a person or community can have a readable identity without a central account provider. - Finding content: Nostr discovery works best when popular relays, search relays, and apps already have the data. That gives the best-connected services extra power over what people find. Bitsocial discovery only helps find peers. It does not need to host the content, own the community, or become the place everyone must trust. ### Bluesky vs Bitsocial Source: https://bitsocial.net/#bluesky-comparison | Topic | Bluesky | Bitsocial | | --- | --- | --- | | Spam replies | Spam is limited by servers, then handled by labels and AppView moderation. | A community can challenge posters first and reject spam before it appears. | | Community rules | The app can filter content, but the community cannot require its own challenge first. | Rules belong to the community, so accepted posts pass the same gate. | | Scaling costs | Growth pushes load onto account servers, relays, AppViews, and feed services. | More readers can also mean more people helping share the content. | | Data hosting | Public posts live on a Personal Data Server, then relays and AppViews index them. | Users can fetch and share posts directly with each other. | | Censorship points | The account host, relay, or AppView can block the normal path to your audience. | A community can stay reachable while peers keep sharing it. | | Who accepts posts? | Community-like spaces are built through feeds, lexicons, or app features, not P2P objects with their own gate. | The community is the thing you post to; it accepts or rejects posts itself. | | First post | The easy path is a hosted account with that provider's limits and rules. | You can start with a local key, without making a server own your identity. | | What your browser is | A client of a PDS and an AppView. | A peer. It runs a node and fetches from other peers. | | Human names | Readable handles are DNS names; the DID document points apps to the account's current server. | Keys stay yours, and .bso names can point to people or communities. | | Finding content | Discovery depends on AppViews, search indexes, feeds, and client defaults. | Discovery finds peers; it does not become the place that owns the content. | Details: - Spam replies: Bluesky uses PDS limits, moderation labels, and AppView choices to reduce abuse. Those defenses are useful, but they are server and indexing-layer defenses. Bitsocial gives the community a front door: complete the challenge, or the reply is never accepted into the thread. - Community rules: Bluesky moderation is built around labels, AppViews, and service rules. That can hide, rank, or limit content, but it is not the same as a community-owned pre-post gate. Bitsocial lets each community choose the gate for posting, such as account age, reputation, captcha, whitelist, payment, or another challenge. - Scaling costs: ATProto deliberately uses servers for convenience and availability. Relays aggregate firehoses, AppViews index the network, and custom feeds often consume the firehose. Official docs describe relays and AppViews as resource intensive, and relay operations have already needed scaling work. Bitsocial uses routers only to find peers; readers can help carry the content. - Data hosting: Bluesky's repo design is one of its strongest parts: public records are signed, portable, and exportable. But the authoritative host is still the current PDS, and the normal app experience depends on relays and AppViews. Bitsocial starts with peer-to-peer content sharing instead of server-hosted account repos. - Censorship points: Bluesky separates account hosting, relay distribution, and AppView indexing. That helps portability, but it also creates several visibility gates that ordinary users rarely change. A PDS can refuse service, relays can stop carrying data, and AppViews can hide or ignore records. Bitsocial keeps reach closer to the community and its peers. - Who accepts posts?: Bluesky can build groups, forums, and community feeds through lexicons, AppViews, and feed generators. That still leaves the community dependent on services that index and interpret records. Bitsocial treats the community as the addressed object: its node validates posts and publishes the accepted state. - First post: Bluesky is easy when bsky.social or another PDS handles accounts, sessions, writes, and hosting. That convenience is server-based, and hosted services apply their own API limits and policies. Bitsocial can create the user's key locally and let the user post through direct P2P or optional hosted access without a platform account owning the identity. - What your browser is: A Bluesky web app talks to a PDS for writes and an AppView for reads, so the browser is always a client of services that hold and interpret the data. Bitsocial browser clients run a Helia node in the page: they ask HTTP routers which peers hold a community, then fetch and publish over libp2p directly. Browser peers dial over secure WebSockets, and the gossipsub sequence-number fix in @libp2p/gossipsub 15.0.21 is what stopped posts published from a tab being discarded by the rest of the network. - Human names: Bluesky has a real improvement over raw public keys: DIDs plus readable handles. The tradeoff is that handles are DNS names and the DID document still advertises the user's current PDS. Bitsocial keeps cryptographic ownership while adding .bso names for people and communities. - Finding content: Bluesky's own architecture puts feeds, search, and aggregate views in AppViews and related services. That makes discovery powerful, but visibility follows whichever aggregators the app uses. Bitsocial discovery services only return peer addresses. They do not need to host the content, own the community, or decide what everyone sees. ### ActivityPub / Fediverse vs Bitsocial Source: https://bitsocial.net/#mastodon-comparison | Topic | ActivityPub / Fediverse | Bitsocial | | --- | --- | --- | | Spam replies | Spam reaches servers first, then filters, reports, and moderators respond. | A community can challenge posters first and reject spam before it appears. | | Community rules | Rules belong to the instance, the user, or a domain blocklist. | Rules belong to the community, so accepted posts pass the same gate. | | Scaling costs | Growth pushes storage, federation, streaming, and moderation cost onto instances. | More readers can also mean more people helping share the content. | | Data hosting | Posts are hosted by an instance, then federated as ActivityPub objects. | Users can fetch and share posts directly with each other. | | Censorship points | Instance admins can limit local accounts or block whole remote servers. | A community can stay reachable while peers keep sharing it. | | Who accepts posts? | A Fediverse community is still defined by the app or instance that accepts and announces its posts. | The community is the thing you post to; it accepts or rejects posts itself. | | First post | You post through the server account you chose. | You can start with a local key, without making a server own your identity. | | What your browser is | A client of the server you signed up to. | A peer. It runs a node and fetches from other peers. | | Human names | Your readable handle includes a server, so moving servers can change your address. | Keys stay yours, and .bso names can point to people or communities. | | Finding content | Search and timelines depend on what your instance has seen, cached, and chosen to show. | Discovery finds peers; it does not become the place that owns the content. | Details: - Spam replies: ActivityPub does not define a universal anti-spam gate. The spec recommends server-side filtering, and Mastodon adds signup limits, email confirmation, reports, blocks, filters, and domain moderation. Those tools help, but they usually work at the server or visibility layer. Bitsocial lets a community challenge a poster before the reply is accepted. - Community rules: Mastodon gives instance admins, moderators, and users many controls: filters, mutes, blocks, reports, server-wide limits, and suspensions. They are powerful federation controls, but they are not a portable community-owned pre-post gate. Bitsocial lets each community choose its posting challenge, so accepted posts pass the same rule before entering the thread. - Scaling costs: A Mastodon server needs a domain, always-online hosting, email, database, Redis, background workers, streaming, and often object storage. Mastodon can scale horizontally, but that means operating more app servers, workers, databases, queues, and moderation capacity. Bitsocial uses routers only to find peers; readers can help carry the content they read. - Data hosting: ActivityPub actors live on servers with inboxes and outboxes. In Mastodon, ActivityPub Notes and Questions become statuses and polls that the instance stores, serves, and delivers to other servers. That is much more open than one central platform, but the normal data host is still an always-online server. Bitsocial starts with peer-to-peer content sharing instead. - Censorship points: Mastodon moderation is local to each server: one admin cannot delete a remote account globally, but they can hide, limit, suspend, or domain-block what their own users see. If influential servers block an instance, reach can collapse for many users. Bitsocial keeps reach closer to the community and the peers still sharing it. - Who accepts posts?: ActivityPub lets different apps interoperate, but the server software still decides what a community means. Mastodon is mostly profile and timeline based. Lemmy adds ActivityPub Groups for forum communities, but the Lemmy instance still accepts, stores, and announces the posts. In Bitsocial, the community is the object you post to: its node checks the posting gate and publishes the accepted thread state. That is the difference: the rule belongs to the community, not just the app or instance hosting it. - First post: Mastodon is easy once a user chooses an instance and creates an account. The server handles sessions, posts, media, federation, and rules. Self-hosting gives more control, but requires operating the stack. Bitsocial can create the user's key locally and let the user post through direct P2P or optional hosted access without a platform account owning the identity. - What your browser is: A Mastodon web client is a front end for the instance that stores your account, your posts, and your timeline. Bitsocial browser clients run a Helia node in the page: they ask HTTP routers which peers hold a community, then fetch and publish over libp2p directly. Browser peers dial over secure WebSockets, and the gossipsub sequence-number fix in @libp2p/gossipsub 15.0.21 is what stopped posts published from a tab being discarded by the rest of the network. - Human names: Mastodon handles are readable because WebFinger turns @user@domain into an ActivityPub actor URI. The domain matters: the same username on another server is a different account. Mastodon can redirect or move followers to a new account, but posts and media do not move. Bitsocial keeps cryptographic ownership and adds .bso names for people and communities. - Finding content: ActivityPub delivers activities to actor inboxes and shared inboxes, so discovery naturally depends on what servers receive, cache, index, and choose to show. Mastodon users can also filter users, keywords, and whole domains. Bitsocial discovery services only return peer addresses; they do not need to host content or decide what everyone sees. ### Farcaster vs Bitsocial Source: https://bitsocial.net/#farcaster-comparison | Topic | Farcaster | Bitsocial | | --- | --- | --- | | Spam replies | Valid signed casts can enter hubs; apps moderate visibility after the fact. | A community can challenge posters first and reject spam before it appears. | | Community rules | Storage rent raises spam cost, but community gates are app or channel policy. | Rules belong to the community, so accepted posts pass the same gate. | | Scaling costs | Growth pushes load onto hubs, storage rent, APIs, and indexers. | More readers can also mean more people helping share the content. | | Data hosting | Identity is onchain, while casts and social messages are stored by hubs or Snapchain. | Users can fetch and share posts directly with each other. | | Censorship points | Clients, APIs, channels, and hub/index access can shape what users actually see. | A community can stay reachable while peers keep sharing it. | | Who accepts posts? | Channels are app/client layers, not communities that accept posts themselves. | The community is the thing you post to; it accepts or rejects posts itself. | | First post | First post usually starts through an app that manages signers, storage, and hubs. | You can start with a local key, without making a server own your identity. | | What your browser is | A client of an app, its hubs, and its indexers. | A peer. It runs a node and fetches from other peers. | | Human names | Farcaster usernames are protocol registrations with app signer keys. | Keys stay yours, and .bso names can point to people or communities. | | Finding content | Discovery depends on clients, channels, APIs, and indexers built over hub data. | Discovery finds peers; it does not become the place that owns the content. | Details: - Spam replies: A Farcaster reply is a signed message accepted when the FID, signer, and storage rules check out. Storage rent limits volume, but it is not a community-owned challenge before each reply. Bitsocial gives the community a front door: complete the challenge, or the reply is never accepted into the thread. - Community rules: Farcaster's storage registry makes spam more costly and clients can filter or moderate channels, but the rule is not owned by each forum-style community as a pre-post gate. Bitsocial lets each community choose the gate for posting, such as account age, reputation, captcha, whitelist, payment, or another challenge. - Scaling costs: Farcaster avoids putting every social action onchain, but hub or Snapchain nodes, storage accounting, APIs, and indexers still need to replicate and serve a large public social graph. Popularity increases those operator costs. Bitsocial uses routers only to find peers; readers can help carry the content they read. - Data hosting: Farcaster keeps the account identity and storage accounting on OP Mainnet, while casts, follows, reactions, and profile updates are signed messages stored and replicated by hubs or Snapchain. That makes it lighter than a fully onchain social network, but the normal data layer is still a server network. Bitsocial starts with peer-to-peer content sharing instead of hub-hosted social state. - Censorship points: Farcaster messages are valid if they satisfy protocol rules, but what users actually see is shaped by clients, APIs, channels, indexers, and moderation systems around the protocol. That reduces global deletion risk, but visibility still follows the popular surfaces. Bitsocial keeps reach closer to the community and the peers still sharing it. - Who accepts posts?: Farcaster's base protocol stores user messages and relationships. Channels and community-like spaces are client or app conventions layered on top, so the posting gate depends on the app and indexing path users choose. Bitsocial treats the community as the addressed object: its node validates posts and publishes the accepted state. - First post: Farcaster's user experience is easiest through a client that manages account setup, app signers, storage, and hub or Snapchain connectivity. Running your own data path is possible, but not the first-post path for normal users. Bitsocial can create the user's key locally and let the user post through direct P2P or optional hosted access without routing the first post through app-managed signer and storage infrastructure. - What your browser is: A Farcaster client reads through APIs and indexers over hub or Snapchain data, and writes through app-managed signers, so the browser never touches the network itself. Bitsocial browser clients run a Helia node in the page: they ask HTTP routers which peers hold a community, then fetch and publish over libp2p directly. Browser peers dial over secure WebSockets, and the gossipsub sequence-number fix in @libp2p/gossipsub 15.0.21 is what stopped posts published from a tab being discarded by the rest of the network. - Human names: Farcaster IDs, custody addresses, signer keys, storage registration, and usernames are anchored onchain. That gives accounts cryptographic portability, but the identity is still a protocol registration with app-managed signer paths and paid storage. Bitsocial keeps cryptographic ownership while adding .bso names for people and communities. - Finding content: Farcaster discovery usually happens through clients, channels, recommendation systems, APIs, and indexed hub or Snapchain data. The protocol data can be open, but the practical discovery layer is still whichever app and indexer the user uses. Bitsocial discovery services only return peer addresses; they do not need to host content or decide what everyone sees. ### Lens vs Bitsocial Source: https://bitsocial.net/#lens-comparison | Topic | Lens | Bitsocial | | --- | --- | --- | | Spam replies | Replies are protocol interactions; app and rule choices determine visibility. | A community can challenge posters first and reject spam before it appears. | | Community rules | Rules can be programmable, but the gate lives in Lens contracts and apps. | Rules belong to the community, so accepted posts pass the same gate. | | Scaling costs | Growth pushes load onto Lens Chain, storage, indexers, RPCs, and app backends. | More readers can also mean more people helping share the content. | | Data hosting | Profiles, graph, feeds, groups, and rules live in Lens smart-contract primitives. | Users can fetch and share posts directly with each other. | | Censorship points | Apps can filter, contracts can enforce rules, and interfaces still decide what users see. | A community can stay reachable while peers keep sharing it. | | Who accepts posts? | Groups and feeds can have rules, but acceptance runs through Lens infrastructure. | The community is the thing you post to; it accepts or rejects posts itself. | | First post | First post usually starts through a Lens app, wallet, RPC, and indexer path. | You can start with a local key, without making a server own your identity. | | What your browser is | A client of a Lens app, an RPC, and an indexer. | A peer. It runs a node and fetches from other peers. | | Human names | Lens names follow wallet, app, and chain infrastructure. | Keys stay yours, and .bso names can point to people or communities. | | Finding content | Discovery depends on Lens apps, APIs, indexers, graphs, feeds, and app defaults. | Discovery finds peers; it does not become the place that owns the content. | Details: - Spam replies: Lens can attach rules to feeds, groups, accounts, and posts, so it can block or restrict some interactions. That is useful, but it is still smart-contract and app rule execution, not a P2P community challenge chosen and run by the community node itself. Bitsocial gives the community a front door: complete the challenge, or the reply is never accepted into the thread. - Community rules: Lens rules are powerful and programmable, but they live as protocol and app primitives on Lens Chain. For a forum-style community, that still means depending on the chain, app, and indexer path to enforce and display the rule. Bitsocial lets each community choose the gate for posting, such as account age, reputation, captcha, whitelist, payment, or another challenge. - Scaling costs: Lens is built for scalable SocialFi with Lens Chain, ZK infrastructure, storage, APIs, and indexers. That is more scalable than posting everything to a general L1, but growth still concentrates load in chain, RPC, indexing, storage, and app infrastructure. Bitsocial uses routers only to find peers; readers can help carry the content they read. - Data hosting: Lens has moved toward Lens Chain and a V3 model with account, feed, graph, group, namespace, action, and rule primitives. That gives developers powerful shared infrastructure, but social state still goes through chain and protocol primitives plus their indexers. Bitsocial starts with peer-to-peer content sharing instead of chain-hosted social state. - Censorship points: Lens can enforce rules in protocol primitives, and each interface can also apply its own terms, filters, and ranking. That gives flexibility, but users still depend on apps, indexers, RPCs, and protocol governance for visibility. Bitsocial keeps reach closer to the community and the peers still sharing it. - Who accepts posts?: Lens is stronger than many social protocols here: V3 includes groups, feeds, namespaces, and rules that can restrict interactions. The tradeoff is that acceptance still runs through Lens chain/protocol primitives, apps, and indexers. Bitsocial treats the community as the addressed object: its node validates posts and publishes the accepted state. - First post: Lens focuses on a web2-like flow with gasless or signless transactions and embedded wallet support. That improves onboarding, but a user is still entering through a Lens app, wallet, RPC, indexer, and chain path. Bitsocial can create the user's key locally and let the user post through direct P2P or optional hosted access without routing the first post through wallet, RPC, indexer, and chain infrastructure. - What your browser is: A Lens app in the browser reads through indexers and writes through wallets, RPCs, and chain infrastructure. Bitsocial browser clients run a Helia node in the page: they ask HTTP routers which peers hold a community, then fetch and publish over libp2p directly. Browser peers dial over secure WebSockets, and the gossipsub sequence-number fix in @libp2p/gossipsub 15.0.21 is what stopped posts published from a tab being discarded by the rest of the network. - Human names: Lens accounts live on Lens Chain, and V3 treats every EVM account as a profile with usernames and social primitives available through apps. That is much friendlier than raw public keys, but the readable identity still follows wallet, chain, app, and indexer infrastructure. Bitsocial keeps cryptographic ownership while adding .bso names for people and communities. - Finding content: Lens gives developers shared graphs, feeds, groups, APIs, and indexers, which is useful for building social apps. But discovery still follows the Lens app, API, indexer, feed, and graph choices users enter through. Bitsocial discovery services only return peer addresses; they do not need to host content or decide what everyone sees. ### DeSo vs Bitsocial Source: https://bitsocial.net/#deso-comparison | Topic | DeSo | Bitsocial | | --- | --- | --- | | Spam replies | A spam comment can become an onchain transaction before filters hide it. | A community can challenge posters first and reject spam before it appears. | | Community rules | Rules mostly filter chain data after transactions already exist. | Rules belong to the community, so accepted posts pass the same gate. | | Scaling costs | Each social action is a transaction, so node and index costs keep climbing. | More readers can also mean more people helping share the content. | | Data hosting | Profiles, posts, comments, follows, and reactions are written as DeSo transactions. | Users can fetch and share posts directly with each other. | | Censorship points | The chain keeps public data; node operators pay to index and filter what users see. | A community can stay reachable while peers keep sharing it. | | Who accepts posts? | A node can curate a feed, but communities are views over global transaction data. | The community is the thing you post to; it accepts or rejects posts itself. | | First post | The easy path signs a social transaction through DeSo Identity and a hosted node API. | You can start with a local key, without making a server own your identity. | | What your browser is | A client of a node API and DeSo Identity. | A peer. It runs a node and fetches from other peers. | | Human names | A DeSo profile follows a public key, but the social history lives on the DeSo chain. | Keys stay yours, and .bso names can point to people or communities. | | Finding content | Discovery depends on nodes and feeds that can afford to index the growing chain. | Discovery finds peers; it does not become the place that owns the content. | Details: - Spam replies: A DeSo comment is an onchain transaction. Nodes and moderation services can filter what they show, but junk can still become part of the shared ledger, adding cost and data that future nodes and indexers have to carry. Bitsocial gives the community a front door: complete the challenge, or the reply is never accepted into the thread. - Community rules: DeSo moderation is intentionally open: every node can choose the subset of public chain data it exposes, and external labelers can participate. That helps filtering, but it usually happens after a signed transaction has entered the shared chain. Bitsocial lets each community choose its posting challenge, so accepted posts pass the same rule before entering the thread. - Scaling costs: DeSo optimizes a blockchain for social storage, but that means posts, comments, likes, follows, and other social actions become transactions. The official node FAQ already describes a large node baseline and database plus transaction-index growth, so popularity raises the hardware and indexing bar for operators. Bitsocial uses routers only to find peers; readers can help carry the content they read. - Data hosting: DeSo's strongest claim is that identities, profiles, posts, comments, likes, follows, and related social data are stored directly on its purpose-built blockchain. That removes a single app database, but it also means each social action becomes transaction-shaped data that nodes and indexers have to keep carrying. Bitsocial starts with peer-to-peer content sharing instead of writing every social action to a global ledger. - Censorship points: DeSo's public chain can keep content from disappearing from every node at once, but node operators still need to run and index the growing ledger before deciding what to expose, hide, rank, or make compliant. Bitsocial keeps reach closer to the community and the peers still sharing it. - Who accepts posts?: DeSo nodes can create their own feeds over the shared firehose, which helps discovery and curation. But the community is still a curated view over global chain transactions, not a P2P object that owns its own acceptance gate before new posts enter. Bitsocial treats the community as the addressed object: its node validates posts and publishes the accepted state. - First post: DeSo web apps commonly rely on DeSo Identity for signing and on a node API for constructing and submitting social transactions. That is easier than running a node, but the post still enters through hosted identity, node, and transaction infrastructure. Bitsocial can create the user's key locally and let the user post through direct P2P or optional hosted access without a platform account owning the identity. - What your browser is: A DeSo web app signs through DeSo Identity and submits through a node API, so the browser depends on hosted identity and node infrastructure. Bitsocial browser clients run a Helia node in the page: they ask HTTP routers which peers hold a community, then fetch and publish over libp2p directly. Browser peers dial over secure WebSockets, and the gossipsub sequence-number fix in @libp2p/gossipsub 15.0.21 is what stopped posts published from a tab being discarded by the rest of the network. - Human names: DeSo identity is controlled by a public key, and apps can sign actions through DeSo Identity. That gives portability across DeSo apps, but the human profile and social history are still DeSo-chain objects that every serious node or indexer has to carry. Bitsocial keeps cryptographic ownership while adding .bso names for people and communities. - Finding content: DeSo nodes can expose different feeds over the same open chain data. That makes curation competitive, but discovery still follows the node or feed view that can keep indexing and serving the growing transaction history. Bitsocial discovery services only return peer addresses; they do not need to host content or decide what everyone sees. ### Steemit vs Bitsocial Source: https://bitsocial.net/#steemit-comparison | Topic | Steemit | Bitsocial | | --- | --- | --- | | Spam replies | A valid comment can enter the chain; voting and moderation happen after. | A community can challenge posters first and reject spam before it appears. | | Community rules | Rules mostly rely on chain resources, voting, reputation, and later moderation. | Rules belong to the community, so accepted posts pass the same gate. | | Scaling costs | Growth pushes more posts, votes, rewards, API load, and moderation work onto Steem infrastructure. | More readers can also mean more people helping share the content. | | Data hosting | Posts, votes, rewards, and account actions are written to the Steem blockchain. | Users can fetch and share posts directly with each other. | | Censorship points | The chain keeps the record, but front ends, votes, witnesses, and communities shape reach. | A community can stay reachable while peers keep sharing it. | | Who accepts posts? | Communities can organize posts, but Steem accounts and chain rules accept them. | The community is the thing you post to; it accepts or rejects posts itself. | | First post | First post starts with Steem account keys and a Steem front end or API node. | You can start with a local key, without making a server own your identity. | | What your browser is | A client of a front end and an API node. | A peer. It runs a node and fetches from other peers. | | Human names | A Steem username is readable, but tied to Steem's account system. | Keys stay yours, and .bso names can point to people or communities. | | Finding content | Discovery depends on front-end feeds, tags, communities, rewards, and ranking. | Discovery finds peers; it does not become the place that owns the content. | Details: - Spam replies: A Steem comment can enter the chain if the account has resources and signs the operation. Communities and votes can suppress it afterward, but the anti-spam path is mostly post-acceptance. Bitsocial gives the community a front door: complete the challenge, or the reply is never accepted into the thread. - Community rules: Steem has account resources, voting, reputation, muting, and community moderation. Those tools can discourage or hide spam, but they usually work through chain economics and post-acceptance curation rather than a community-owned pre-post gate. Bitsocial lets each community choose its posting challenge, so accepted posts pass the same rule before entering the thread. - Scaling costs: Steem offers fast blocks and resource-based free transactions, but scaling still means more chain history, more API node work, more witness infrastructure, and more front-end moderation and ranking load. Bitsocial uses routers only to find peers; readers can help carry the content they read. - Data hosting: Steemit runs on Steem, so posts, comments, votes, rewards, and account operations are blockchain actions served through Steem nodes and front ends. That makes the record public and portable across Steem apps, but the normal data layer is still the Steem chain plus API nodes. Bitsocial starts with peer-to-peer content sharing instead. - Censorship points: Steem makes the chain record hard to erase, but front ends, witnesses, community moderators, downvotes, reputation, and reward rules shape what people find and trust. Bitsocial keeps reach closer to the community and the peers still sharing it. - Who accepts posts?: Steem communities can organize posts and mute content through community operations, and front ends can choose what to show. The underlying post is still accepted as a Steem account and chain operation, not by a P2P community object with its own gate. Bitsocial treats the community as the addressed object: its node validates posts and publishes the accepted state. - First post: Steemit is straightforward once a user has a Steem account and posting key, but that account belongs to the Steem permission model and the post enters through a Steem front end or API node. Bitsocial can create the user's key locally and let the user post through direct P2P or optional hosted access without a platform account owning the identity. - What your browser is: A Steem front end reads and writes through API nodes that carry the chain, so the browser is a client of whichever node it points at. Bitsocial browser clients run a Helia node in the page: they ask HTTP routers which peers hold a community, then fetch and publish over libp2p directly. Browser peers dial over secure WebSockets, and the gossipsub sequence-number fix in @libp2p/gossipsub 15.0.21 is what stopped posts published from a tab being discarded by the rest of the network. - Human names: Steem accounts are readable names with hierarchical keys, including posting, active, owner, and memo permissions. That is practical for a blockchain app, but the identity is still a Steem account rather than a portable person or community name. Bitsocial keeps cryptographic ownership while adding .bso names for people and communities. - Finding content: Steemit discovery is driven by front-end feeds, tags, communities, rewards, votes, and ranking. Those mechanisms are useful, but they make visibility depend on Steem-specific economics and interfaces. Bitsocial discovery services only return peer addresses; they do not need to host content or decide what everyone sees. ## Documentation map ## Protocol notes - [Browser Peer-to-Peer](https://docs.bitsocial.net/browser-p2p/): How a Bitsocial web app runs a real libp2p node in the browser tab, which transports it uses, and the 2026 upstream fix that made publishing from a tab work. - [Custom Anti-Spam Challenges](https://docs.bitsocial.net/custom-challenges/): Why Bitsocial lets each community define its own anti-spam policy. - [Identity and Community Ownership](https://docs.bitsocial.net/identity-and-ownership/): How Bitsocial treats profiles and communities as key-controlled property instead of revocable platform accounts. - [Local Moderation, Not Global Bans](https://docs.bitsocial.net/local-moderation/): What Bitsocial means when it says moderation stays local. - [Peer-to-Peer Protocol](https://docs.bitsocial.net/peer-to-peer-protocol/): How Bitsocial uses IPFS/libp2p, public-key addressing, peer-to-peer pubsub, and browser P2P nodes to deliver serverless social media. ## Master plan - [Bitsocial Chain](https://docs.bitsocial.net/bitsocial-network/): Phase 2 of the master plan, covering the proposed Ethereum L2 appchain economic layer for Bitsocial apps. - [Build your own Bitsocial client](https://docs.bitsocial.net/build-your-own-client/): Builder guide for shipping independent Bitsocial clients, from imageboards and forums to niche social apps. - [Decentralize All Social Media](https://docs.bitsocial.net/decentralize-all-social-media/): Phase 5 of the master plan, focused on funding and building the long tail of Bitsocial social clients. - [Decentralize Twitter/X](https://docs.bitsocial.net/flagship-bitsocial-app/): Phase 3 of the master plan: a focused, decentralized Twitter/X alternative for text-first public conversation with replaceable infrastructure. - [Scale Bitsocial Economies](https://docs.bitsocial.net/scale-bitsocial-economies/): Phase 4 of the master plan, focused on funding, infrastructure pluralism, and competing services around Bitsocial. ## Apps - [5chan](https://docs.bitsocial.net/apps/5chan/): A serverless, decentralized imageboard built on the Bitsocial protocol where anyone can create and own boards. - [Seedit](https://docs.bitsocial.net/apps/seedit/): A serverless, decentralized Reddit-style forum built on the Bitsocial protocol where anyone can create and own communities. ## Developer tools - [Bitsocial CLI](https://docs.bitsocial.net/developer-tools/cli/): Command-line interface for running a Bitsocial node, creating communities, and managing protocol operations. - [React Hooks](https://docs.bitsocial.net/developer-tools/react-hooks/): React hooks library for building decentralized social applications on the Bitsocial protocol. ## Anti-spam challenges - [Captcha Canvas Challenge](https://docs.bitsocial.net/anti-spam/captcha-canvas-challenge/): Standalone image-based captcha challenge for Bitsocial communities. - [EVM Contract Call Challenge](https://docs.bitsocial.net/anti-spam/evm-contract-call/): Anti-spam challenge that verifies on-chain conditions by calling an EVM smart contract. - [Spam Blocker](https://docs.bitsocial.net/anti-spam/spam-blocker/): Centralized spam detection service with risk scoring, OAuth challenges, and configurable tier thresholds. - [Voucher Challenge](https://docs.bitsocial.net/anti-spam/voucher-challenge/): Anti-spam challenge that gates publishing behind unique voucher codes distributed by community owners. ## Infrastructure - [BSO Resolver](https://docs.bitsocial.net/infrastructure/bso-resolver/): Resolve .bso domain names to public keys via Bitsocial TXT records. - [Mintpass](https://docs.bitsocial.net/infrastructure/mintpass/): NFT-based authentication system that helps Bitsocial communities verify users and reduce sybil attacks. - [Pubsub Provider](https://docs.bitsocial.net/infrastructure/pubsub-provider/): Fallback pubsub relay and delegated routing provider for Bitsocial operators. - [Telegram Bots](https://docs.bitsocial.net/infrastructure/telegram-bots/): Feed bots that monitor Bitsocial community lists and forward posts to Telegram channels. ## Contributor playbooks - [Bug Investigation Workflow](https://docs.bitsocial.net/agent-playbooks/bug-investigation/): Use this when a bug is reported in a specific file/line/code block. - [Commit and Issue Format](https://docs.bitsocial.net/agent-playbooks/commit-issue-format/): Use this when proposing or implementing meaningful code changes. - [Agent Hooks Setup](https://docs.bitsocial.net/agent-playbooks/hooks-setup/): If your AI coding assistant supports lifecycle hooks, configure these for this repo. - [Known Surprises](https://docs.bitsocial.net/agent-playbooks/known-surprises/): This file tracks repository specific confusion points that caused agent mistakes. - [Long-Running Agent Workflow](https://docs.bitsocial.net/agent-playbooks/long-running-agent-workflow/): Use this playbook when a task is likely to span multiple sessions, handoffs, or spawned agents. - [Skills and Tools](https://docs.bitsocial.net/agent-playbooks/skills-and-tools/): Use this playbook when setting up/adjusting skills and external tooling. - [Feature List Template](https://docs.bitsocial.net/agent-playbooks/templates/feature-list-template/): Machine-readable template used for long-running multi-session task tracking. - [Progress Log](https://docs.bitsocial.net/agent-playbooks/templates/progress.template/): Append one entry per session. - [Translations Workflow](https://docs.bitsocial.net/agent-playbooks/translations/): This project uses i18next translation files in public/translations/{lang}/default.json. ## Other - [Content Discovery](https://docs.bitsocial.net/content-discovery/): How Bitsocial separates peer discovery from app-level curation. - [i18n Default Language Policy](https://docs.bitsocial.net/i18n-default-language-policy/): Anonymous visitors resolve language in this order: - [A complete layman explanation of the Bitsocial protocol](https://docs.bitsocial.net/layman-protocol-explanation/): A plain-English walkthrough of Bitsocial communities, peer lookup, publishing, anti-spam challenges, moderation, and apps. - [BSO Token History](https://docs.bitsocial.net/token-history/): The full generation history of the BSO token, from its 2021 Avalanche origin to today's immutable, adminless Ethereum contract. ## Full docs corpus ### Browser Peer-to-Peer Source: https://docs.bitsocial.net/browser-p2p/ Path: docs/browser-p2p.md Description: How a Bitsocial web app runs a real libp2p node in the browser tab, which transports it uses, and the 2026 upstream fix that made publishing from a tab work. # Browser Peer-to-Peer A Bitsocial web app does not have to be a client of somebody's server. It can run a [Helia](https://helia.io/) node inside the browser tab, join the same peer-to-peer network as desktop and CLI nodes, fetch community content from peers, and publish over pubsub. This page explains what that actually means, which transports it uses, what it still cannot do, and why publishing from a tab only started working in 2026. For the wider network design, see [Peer-to-Peer Protocol](https://docs.bitsocial.net/peer-to-peer-protocol/). ## What runs in the tab When browser P2P is active, the page holds a real libp2p node: - it dials other peers over secure WebSockets - it fetches and verifies community content from those peers, not from an IPFS gateway - it participates in gossipsub, so publishing a post does not need a hosted pubsub provider - it uses the same protocol client stack (`pkc-js`) as every other Bitsocial app The practical consequence is that no gateway operator sits between a web reader and a community. There is no single HTTPS endpoint that can be pressured into dropping a community for every browser user at once. ## How browser nodes connect `pkc-js` dials peers over **secure WebSockets**. WebRTC and WebTransport dials are denied by default through a connection gater, because in the browser they add long, often-failing connection-establishment paths — STUN/ICE negotiation, certhash rotation — that slow page loads, while WebSocket gives a direct and reliable transport. Callers that specifically want WebRTC or WebTransport can override the gater through `libp2pJsClientsOptions[].libp2pOptions.connectionGater`. The practical consequence is that a browser peer connects to nodes exposing a WSS endpoint, which means those nodes need a domain and a CA-signed certificate. Peers behind consumer connections without one are reached indirectly rather than dialed from the tab. ## Why publishing from the browser only started working in 2026 Browser peer-to-peer is not a new idea. What changed in 2026 is that a browser node's _posts_ now reach the rest of the network. The libp2p pubsub spec requires a message `seqno` to be a linearly increasing 64-bit big-endian integer. `js-libp2p-gossipsub` generated 8 random bytes instead, while go-libp2p-pubsub and rust-libp2p both used a counter. Kubo 0.40+ enables `BasicSeqnoValidator` by default, which rejects any message whose seqno is not greater than the highest already seen from that peer. The effect was that most messages published by a JavaScript node — including a browser node — were silently discarded by Kubo peers. A reproducer measured 2 to 8 of 30 messages arriving. This was diagnosed in [js-libp2p-gossipsub#545](https://github.com/ChainSafe/js-libp2p-gossipsub/issues/545) and fixed in **`@libp2p/gossipsub` 15.0.21** in May 2026. Until that landed, a browser node could connect and read, but its posts mostly vanished on the way to Go peers. `pkc-js` ships `@libp2p/gossipsub` 16.0.4, past that fix. ## What a browser node still cannot do A browser node is a real peer, not a server. It has different limits from a desktop or always-on node: - it usually cannot accept arbitrary inbound connections from the public internet - it only works while the tab is open, so it is not a long-lived host for a community's data - it cannot join a libp2p DHT, which is why discovery goes through HTTP routers - it is a poor fit for seeding at scale Full community hosting is still best handled by a desktop app, `bitsocial-cli`, or another always-on node. Browser P2P changes who can _read and post_ without a gateway; it does not remove the need for peers that stay online. ## HTTP routers are not gateways Browser clients still query [HTTP routers](https://docs.bitsocial.net/peer-to-peer-protocol/#public-key-based-addressing) to find out which peers currently provide a community's address. This is the honest asterisk on "pure peer-to-peer in the browser," and it is worth being precise about it: - a router stores only peer addresses for a content address - it does not store, serve, or even know the community's content - clients query several routers in parallel and merge the results - anyone can run one, and swapping routers is a config change with no data migration After discovery, content transfer and pubsub traffic move peer-to-peer. A router that disappears costs you a lookup path, not your data. An IPFS gateway, by contrast, is in the content path. ## Where this runs today - [5chan](https://docs.bitsocial.net/apps/5chan/) runs pure browser P2P by default in the web app at [5chan.app](https://5chan.app). ## Gateway fallback Gateway-backed access still exists as a compatibility path for browsers or networks that cannot join directly. See [Gateway fallback](https://docs.bitsocial.net/peer-to-peer-protocol/#gateway-fallback). The target architecture is browser P2P first, with gateways as an optional fallback rather than the default bottleneck. ### Custom Anti-Spam Challenges Source: https://docs.bitsocial.net/custom-challenges/ Path: docs/custom-challenges.md Description: Why Bitsocial lets each community define its own anti-spam policy. # Custom Anti-Spam Challenges Bitsocial does not assume one universal spam defense. Each community can decide what a valid post or reply requires, and that challenge can be as lightweight or opinionated as the operator wants. For the full plain-English flow from community addresses to challenge-gated posting, read [A complete layman explanation of the Bitsocial protocol](https://docs.bitsocial.net/layman-protocol-explanation/). ## What a challenge can be - A captcha - An account-age or reputation check - SMS verification - A small payment - A token or NFT requirement - IP-based rules - An allowlist - Any other policy that can be expressed in code ## Why this matters Spam resistance stays local to the community instead of being outsourced to a protocol-wide moderation authority. The node that hosts the community can communicate the challenge peer-to-peer, which means communities can adapt their defenses without forcing the entire network into one policy. ## What this changes On a centralized platform, anti-spam policy usually becomes product policy. When that happens, one company decides what counts as a valid account, a valid post, or acceptable reach. Bitsocial breaks that coupling: - the protocol stays neutral - the community chooses the challenge - apps decide how to present or abstract that challenge This is a better fit for a network meant to host many kinds of social products rather than one global default. ### Identity and Community Ownership Source: https://docs.bitsocial.net/identity-and-ownership/ Path: docs/identity-and-ownership.md Description: How Bitsocial treats profiles and communities as key-controlled property instead of revocable platform accounts. # Identity and Community Ownership Profiles and communities in Bitsocial are controlled by keypairs, not by revocable platform accounts. That changes the relationship between ownership and service providers. You can delegate uptime, hosting, or convenience to another service without giving that service permanent custody over the identity or community itself. ## Why keys matter here A hosted platform account is ultimately rented. It exists as long as the platform continues to honor it. A key-controlled identity is different: - the private key is the source of authority - hosting can be replaced - app choice can change - ownership does not need to move with infrastructure ## What delegation should mean Delegation should mean help with availability, not surrender of control. For example, a public RPC or hosting service can keep a node online, but that service should not need the power to permanently seize the profile or community. Assistance is useful. Custody should remain optional. ## The broader point This makes a Bitsocial identity feel closer to a wallet-controlled asset than to a username rented from a company. The service layer can be swapped out. The owner relationship should survive that swap. ### Local Moderation, Not Global Bans Source: https://docs.bitsocial.net/local-moderation/ Path: docs/local-moderation.md Description: What Bitsocial means when it says moderation stays local. # Local Moderation, Not Global Bans Bitsocial is not moderation-free. It is moderation without a protocol-level super-admin. Community owners still set rules for their own spaces. Apps still decide what they index, rank, or highlight. What Bitsocial does not include is a global authority that can confiscate a community or erase an identity from the network itself. For a broader plain-English walkthrough of community ownership, publishing, challenges, and moderator signatures, read [A complete layman explanation of the Bitsocial protocol](https://docs.bitsocial.net/layman-protocol-explanation/). ## How moderation works - A community owner can moderate that community - An app can choose what it indexes or shows - A user can move to another community or another app If one app filters you, another app can still choose to show you. If one community rejects you, another community can accept you, or you can create your own. ## What Bitsocial avoids Bitsocial avoids the familiar platform choke point where one company controls: - the social graph - the moderation database - the discovery surface - the account namespace That central point is what turns a product policy decision into a network-wide ban. ## The practical tradeoff Local moderation does not mean every app will look identical. It means the consequences of moderation remain scoped: - community rules apply inside that community - app rules apply inside that app - neither automatically becomes protocol law That is what “no global bans” is trying to preserve. ### Peer-to-Peer Protocol Source: https://docs.bitsocial.net/peer-to-peer-protocol/ Path: docs/peer-to-peer-protocol.md Description: How Bitsocial uses IPFS/libp2p, public-key addressing, peer-to-peer pubsub, and browser P2P nodes to deliver serverless social media. # Peer-to-Peer Protocol Bitsocial does not use a blockchain, a federation server, or a centralized backend. Instead it uses the IPFS/libp2p stack to combine two ideas: **public-key-based addressing** and **peer-to-peer pubsub**. Together they let anyone host a community from consumer hardware while users read and post without accounts on any company-controlled service. For a less technical walkthrough, read [A complete layman explanation of the Bitsocial protocol](https://docs.bitsocial.net/layman-protocol-explanation/). ## Does Bitsocial use IPFS? Yes. Bitsocial nodes use IPFS/libp2p primitives for the peer-to-peer layer: public-key-addressed community records, content transfer between peers, and gossipsub pubsub for real-time messages. When these docs say "pubsub," they mean IPFS/libp2p pubsub, not a separate centralized message broker. The protocol currently describes discovery through HTTP routers because Bitsocial clients query router endpoints for provider peer addresses instead of relying on a browser-hostile DHT for every lookup. Routers only return peers; content transfer and pubsub traffic still move through the peer-to-peer network. ## The two problems A decentralized social network has to answer two questions: 1. **Data** — how do you store and serve the world's social content without a central database? 2. **Spam** — how do you prevent abuse while keeping the network free to use? Bitsocial solves the data problem by skipping the blockchain entirely: social media does not need global transaction ordering or permanent availability of every old post. It solves the spam problem by letting each community run its own anti-spam challenge over the peer-to-peer network. For the discovery model above this network layer, see [Content Discovery](https://docs.bitsocial.net/content-discovery/). --- ## Public-key-based addressing {#public-key-based-addressing} In BitTorrent, a file's hash becomes its address (_content-based addressing_). Bitsocial uses a similar idea with public keys: the hash of a community's public key becomes its network address. ```mermaid graph LR K["🔑 Community keypair"] --> H["#️⃣ Hash of public key"] H --> D["🌐 HTTP router(s) lookup of hash"] D --> P["🔌 Provider peer addresses"] P --> C["📄 Latest community content from peers"] ``` Any peer on the network can query an **HTTP router** for that address: the router replies with a list of peer network addresses currently providing the hash of the community, and the client connects to those peers directly to fetch the community's latest state. Each time the content is updated, its version number increases. The network only keeps the latest version — there is no need to preserve every historical state, which is what makes this approach lightweight compared to a blockchain. > **What an HTTP router actually holds.** An HTTP router is a thin index. For each content address > it knows, it stores only the network addresses of peers that announced themselves as providers > (IP/port pairs, libp2p multiaddrs, that sort of thing). It does **not** store the community's > content, its metadata, post text, member list, or even the human-readable label of what is at > that address; it just answers "which peers claim to have this hash?". This makes routers cheap to > run, easy to swap, and not liable for what users publish, similar to a BitTorrent tracker but > without torrent metadata: a tracker maps infohashes to peers, while an HTTP router only maps a > content address to provider peer addresses. > > For redundancy, the client queries **several HTTP routers in parallel** and merges the provider > lists it gets back. Anyone can run a router, and replacing or adding routers is a config change > with no data migration. > > Bitsocial uses HTTP routers instead of a DHT because running a DHT at the scale needed for > content discovery is expensive, especially for mobile. A DHT also does not work in the browser, > since browsers cannot join a libp2p DHT directly. An HTTP router runs cheaply on commodity HTTP > infrastructure and works equally well from a phone or a browser. ### What gets stored at the address The community address does not contain full post content directly. Instead it stores a list of content identifiers — hashes that point to the actual data. The client then fetches each piece of content directly from the peers returned by the HTTP routers. The routers themselves never see or store the content. ```mermaid graph TD A["📍 Community address"] --> M["📋 Metadata
(title, description, rules,
moderator list, challenge config)"] A --> P["🔗 Content pointers
(hashes of latest posts)"] P --> P1["📝 Post 1 content"] P --> P2["📝 Post 2 content"] P --> P3["📝 Post 3 content"] ``` At least one peer always has the data: the community operator's node. If the community is popular, many other peers will have it too and the load distributes itself, the same way popular torrents are faster to download. --- ## Peer-to-peer pubsub Pubsub (publish-subscribe) is a messaging pattern where peers subscribe to a topic and receive every message published to that topic. Bitsocial uses a peer-to-peer pubsub network — anyone can publish, anyone can subscribe, and there is no central message broker. To publish a post to a community, a user publishes a message whose topic equals the community's public key. The community operator's node picks it up, validates it, and — if it passes the anti-spam challenge — includes it in the next content update. --- ## Anti-spam: challenges over pubsub An open pubsub network is vulnerable to spam floods. Bitsocial solves this by requiring publishers to complete a **challenge** before their content is accepted. The challenge system is flexible: each community operator configures their own policy. Options include: | Challenge type | How it works | | ----------------- | ------------------------------------------------- | | **Captcha** | Visual or interactive puzzle presented in the app | | **Rate limiting** | Limit posts per time window per identity | | **Token gate** | Require proof of balance of a specific token | | **Payment** | Require a small payment per post | | **Allowlist** | Only pre-approved identities can post | | **Custom code** | Any policy expressible in code | Peers that relay too many failed challenge attempts get blocked from the pubsub topic, which prevents denial-of-service attacks on the network layer. --- ## Lifecycle: reading a community This is what happens when a user opens the app and views a community's latest posts. ```mermaid sequenceDiagram participant User as 👤 User app participant Routers as 🌐 HTTP routers participant Node as 🖥️ Community node User->>Routers: Query community address (in parallel) Note over Routers: Each router returns
peer addresses only, never content Routers-->>User: Return provider peer addresses User->>Node: Connect to peer, fetch latest pointers + metadata Node-->>User: Return latest content pointers + metadata User->>Node: Fetch post content by hash Node-->>User: Return post data Note over User: Render posts in
familiar social UI Note over User,Node: Multiple community queries
run concurrently ``` **Step by step:** 1. The user opens the app and sees a social interface. 2. The client queries several HTTP routers in parallel for each community the user follows; each router returns peer addresses only, never content. Query latency depends on network conditions and router load; under typical low-latency conditions, queries often return within about one second and run concurrently. 3. Once the client has peer addresses, it connects to those peers and fetches the community's latest content pointers and metadata (title, description, moderator list, challenge configuration). 4. The client fetches the actual post content using those pointers, then renders everything in a familiar social interface. --- ## Lifecycle: publishing a post Publishing involves a challenge-response handshake over pubsub before the post is accepted. ```mermaid sequenceDiagram participant User as 👤 User app participant Pubsub as 📡 Pubsub network participant Node as 🖥️ Community node Note over User: Generate keypair
(if first post) User->>Pubsub: Join community topic User->>Pubsub: Request challenge Pubsub->>Node: Relay challenge request Node-->>Pubsub: Send challenge (e.g. captcha) Pubsub-->>User: Relay challenge to user Note over User: User completes challenge User->>Pubsub: Submit post + challenge answer Pubsub->>Node: Relay post + answer Note over Node: Validate answer Node-->>Pubsub: Broadcast result (accepted/rejected) Pubsub-->>User: Relay result Note over Node: Update community content
at public-key address Note over User,Node: Other readers receive
the update within minutes ``` **Step by step:** 1. The app generates a keypair for the user if they don't have one yet. 2. The user writes a post for a community. 3. The client joins the pubsub topic for that community (keyed to the community's public key). 4. The client requests a challenge over pubsub. 5. The community operator's node sends back a challenge (for example, a captcha). 6. The user completes the challenge. 7. The client submits the post along with the challenge answer over pubsub. 8. The community operator's node validates the answer. If correct, the post is accepted. 9. The node broadcasts the result over pubsub so network peers know to continue relaying messages from this user. 10. The node updates the community's content at its public-key address. 11. Within a few minutes, every reader of the community receives the update. --- ## Architecture overview The full system has three layers that work together: ```mermaid graph TB subgraph Apps ["App layer"] A1["🖥️ Desktop app"] A2["🌐 Web app"] A3["📱 Mobile app"] end subgraph Protocol ["Protocol layer"] PK["🔑 Public-key addressing"] PS["📡 Peer-to-peer pubsub"] CH["🛡️ Challenge system"] end subgraph Network ["Network layer"] Router["🛰️ HTTP router
(content discovery)"] GS["💬 Gossipsub
(real-time messaging)"] TR["📦 Content transfer
(data exchange)"] end A1 & A2 & A3 --> PK & PS & CH PK --> Router PS --> GS CH --> GS PK --> TR ``` | Layer | Role | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | **App** | User interface. Multiple apps can exist, each with its own design, all sharing the same communities and identities. | | **Protocol** | Defines how communities are addressed, how posts are published, and how spam is prevented. | | **Network** | The underlying peer-to-peer infrastructure: HTTP routers for discovery, gossipsub for real-time messaging, and content transfer for data exchange. | --- ## Privacy: unlinking authors from IP addresses When a user publishes a post, the content is **encrypted with the community operator's public key** before it enters the pubsub network. This means that while network observers can see that a peer published _something_, they cannot determine: - what the content says - which author identity published it This is similar to how BitTorrent makes it possible to discover which IPs seed a torrent but not who originally created it. The encryption layer adds an additional privacy guarantee on top of that baseline. --- ## Browser peer-to-peer Browser P2P is now possible in Bitsocial clients. A browser app can run a [Helia](https://helia.io/) node, use the same Bitsocial protocol client stack as other apps, and fetch content from peers instead of asking a centralized IPFS gateway to serve it. The browser can also participate in pubsub directly, so posting does not need a platform-owned pubsub provider in the happy path. This is the important milestone for web distribution: a normal HTTPS website can open into a live P2P social client. Users do not need to install a desktop app before they can read from the network, and the app operator does not need to run a central gateway that becomes the censorship or moderation chokepoint for every browser user. The browser path has different limits from a desktop or server node: - a browser node usually cannot accept arbitrary inbound connections from the public internet - it can load, validate, cache, and publish data while the app is open - it should not be treated as the long-lived host for a community's data - full community hosting is still best handled by a desktop app, `bitsocial-cli`, or another always-on node HTTP routers still matter for content discovery: they return provider addresses for a community hash. They are not IPFS gateways, because they do not serve the content itself. After discovery, the browser client connects to peers and fetches the data through the P2P stack. Browser P2P is now the default web path, not an experiment behind a switch. 5chan runs pure browser P2P by default at 5chan.app, and the Bitsocial blog on bitsocial.net does the same. Browser peers dial over secure WebSockets; `pkc-js` denies WebRTC and WebTransport dials by default because their connection-establishment paths are slow and unreliable in the browser. The upstream change that made browser publishing practical in 2026 was the gossipsub sequence-number fix in `@libp2p/gossipsub` 15.0.21, which stopped Kubo peers from discarding messages published by JavaScript nodes. For the full picture, including what a browser node still cannot do, see [Browser Peer-to-Peer](https://docs.bitsocial.net/browser-p2p/). ## Gateway fallback {#gateway-fallback} Gateway-backed browser access is still useful as a compatibility and rollout fallback. A gateway can relay data between the P2P network and a browser client when a browser cannot join the network directly or when the app intentionally chooses the older path. These gateways: - can be run by anyone - do not require user accounts or payments - do not gain custody over user identities or communities - can be swapped out without losing data The target architecture is browser P2P first, with gateways as an optional fallback rather than the default bottleneck. --- ## Why not a blockchain? Blockchains solve the double-spend problem: they need to know the exact order of every transaction to prevent someone from spending the same coin twice. Social media does not have a double-spend problem. It does not matter if post A was published one millisecond before post B, and old posts do not need to be permanently available on every node. By skipping the blockchain, Bitsocial avoids: - **gas fees** — posting is free - **throughput limits** — no block size or block time bottleneck - **storage bloat** — nodes only keep what they need - **consensus overhead** — no miners, validators, or staking required The tradeoff is that Bitsocial does not guarantee permanent availability of old content. But for social media, that is an acceptable tradeoff: the community operator's node holds the data, popular content spreads across many peers, and very old posts naturally fade — the same way they do on every social platform. ## Why not federation? Federated networks (like email or ActivityPub-based platforms) improve on centralization but still have structural limitations: - **Server dependency** — each community needs a server with a domain, TLS, and ongoing maintenance - **Admin trust** — the server admin has full control over user accounts and content - **Fragmentation** — moving between servers often means losing followers, history, or identity - **Cost** — someone has to pay for hosting, which creates pressure toward consolidation Bitsocial's peer-to-peer approach removes the server from the equation entirely. A community node can run on a laptop, a Raspberry Pi, or a cheap VPS. The operator controls moderation policy but cannot seize user identities, because identities are keypair-controlled, not server-granted. ## What about Nostr? Nostr does not fit cleanly into either bucket. It is not ActivityPub-style federation, because users are not issued accounts by instances and identity is not tied to one server. It is not blockchain social media either, because there is no chain, consensus, gas, or global transaction order. Nostr is better described as **relay-based social media**. In the base protocol ([NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md)), users hold keypairs, sign events, and publish those events to WebSocket relays. Clients subscribe to relays with filters, fetch matching events, and verify signatures locally. Users can also publish relay-list metadata ([NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md)) that tells clients which relays they normally write to and which relays they prefer for reading mentions. That puts Nostr closer to Bitsocial than federated or blockchain systems in one important way: identity is cryptographic and portable. The main difference is the data layer. In Nostr, relays are the normal storage and delivery layer. In Bitsocial, HTTP routers only help clients find peers. The routers do not store posts, profiles, community metadata, or moderation state; they return provider peer addresses, then clients fetch content from peers. Communities show the same split. Nostr has optional patterns for [relay-based groups](https://github.com/nostr-protocol/nips/blob/master/29.md) and [moderator-approved communities](https://github.com/nostr-protocol/nips/blob/master/72.md), but they still depend on relay policy, relay-hosted group state, or client choices about which approvals to honor. Bitsocial treats communities as first-class cryptographic objects whose operator node validates posts, runs the community's challenge policy, and publishes the latest accepted state into the peer-to-peer network. | Question | Nostr | Bitsocial | | ------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | Category | Relay-based protocol | Peer-to-peer community network | | Identity | User public key | User and community keypairs | | Data path | Signed events published to relays | Public-key address resolves to peers; content fetched from peers | | Who keeps it online | Relays chosen by users and clients | Community owner node plus helper seeders | | Communities | Optional relay-based groups or moderator-approved communities | First-class community objects with operator-controlled moderation | | Anti-spam | Relay policy, auth, payment, proof-of-work, client filters, or moderator approvals | Community-defined challenge logic before inclusion | | Main tradeoff | Portable identity, but relay-dependent availability and policy | Less relay dependence, but old content is not guaranteed forever | --- ## Summary Bitsocial is built on two primitives: public-key-based addressing for content discovery, and peer-to-peer pubsub for real-time communication. Together they produce a social network where: - communities are identified by cryptographic keys, not domain names - content spreads across peers like a torrent, not served from a single database - spam resistance is local to each community, not imposed by a platform - users own their identities through keypairs, not through revocable accounts - the whole system runs without servers, blockchains, or platform fees ### Bitsocial Chain Source: https://docs.bitsocial.net/bitsocial-network/ Path: docs/bitsocial-network.md Description: Phase 2 of the master plan, covering the proposed Ethereum L2 appchain economic layer for Bitsocial apps. # Bitsocial Chain Bitsocial Chain is the proposed Ethereum L2 appchain economic layer for Bitsocial apps. The current chain-specific site is [chain.bitsocial.net](https://chain.bitsocial.net). The peer-to-peer social layer lets communities, identities, and content move outside a central platform database. Bitsocial Chain is meant to add the shared naming, monetization, and payment primitives that make those apps harder to starve financially. ## What it is meant to power - decentralized Bitsocial domains such as `.bso` - awards and tipping - durable monetization rails - shared liquidity across apps - financial structures that are harder for banks or platforms to choke off - network effects that do not depend on one company owning the whole stack The goal is not to lead with token mechanics. The goal is to make useful social apps more durable, more fundable, and less dependent on centralized payment or naming providers. ## Current proof of concept The first Bitsocial Chain proof of concept focuses on native `.bso` names. It proves that a name registry can be derived from Ethereum L1 history without placing social content on-chain: - users submit register, update, transfer, and revoke intents through plain Ethereum L1 transactions - anyone can run the derivation node and reconstruct the same `.bso` registry state - a resolver maps a `.bso` name to the Bitsocial public key that clients already use over the peer-to-peer protocol - posts, votes, moderation, feeds, and community content stay off-chain and peer-to-peer That proof of concept is not a production Stage 2 launch. It has no proof system, challenge game, audited code, live deployment, final pricing, or final governance yet. Its long-term posture is transparent by default and privacy-compatible by design: the core chain is public, while future tipping, payments, awards, and liquidity should avoid forcing permanent links between social identity and wallet history. ## Why it matters Decentralizing communities and identities is necessary, but it is not enough to decentralize all of social media. If social apps still depend on a few centralized economic rails, they remain easy to pressure, deplatform, or starve financially. Bitsocial Chain is the proposed answer to that second layer of dependency. ## Relationship to apps Bitsocial Chain should sit under Bitsocial apps, not replace them. The public-facing result should be: - communities stay peer-to-peer - apps stay differentiated - users get practical naming and monetization features - creators and communities can receive support across clients - value can move across the ecosystem without recreating a centralized platform owner ## Why this sits early The current master plan places Bitsocial Chain immediately after the first wedge categories: imageboards, forums, and the public RPC layer that makes those apps practical for more users. That timing matters because social apps need strong network effects. If naming, support, awards, tipping, and monetization arrive too late, centralized competitors keep their biggest advantage for too long. ## Design principles Because Bitsocial Chain is still proposed infrastructure rather than a launched product, the plan should stay disciplined: - Apps and communities first. The network layer should make real social products stronger. - Practical features first. Names, awards, tipping, and payments are easier to explain than abstract financial architecture. - Real contribution over hype. Economic primitives should reward participation, building, and community support. - Curation is allowed. Apps can shape rankings, defaults, and discovery to favor durable communities. - Exact mechanics are still open. This page explains the role of Bitsocial Chain, not a locked promise about final economics. ### Build your own Bitsocial client Source: https://docs.bitsocial.net/build-your-own-client/ Path: docs/build-your-own-client.md Description: Builder guide for shipping independent Bitsocial clients, from imageboards and forums to niche social apps. # Build your own Bitsocial client Bitsocial does not win by having one official app for every use case. It wins when many clients can share the same protocol while competing on interface, culture, discovery, defaults, and business model. 5chan and Seedit are early proof points, not a ceiling. A builder should be able to ship a new imageboard, a forum, a profile client, a mobile-first social app, a niche community tool, or a centralized client that uses Bitsocial underneath without asking permission from a platform owner. ## What builders can change A Bitsocial client can compete on product decisions without forking the whole network: - interface and visual language - onboarding flow - community defaults - moderation surfaces - discovery model - media experience - mobile, desktop, or low-bandwidth constraints - monetization and business model The common layer is the protocol. The product layer is open to competition. ## Fastest way to learn Start with the apps that already exist: - Try [5chan](https://5chan.app) for anonymous imageboard communities. - Try [Seedit](https://seedit.app) for Reddit-style discussion. - Read the [Bitsocial React hooks](https://docs.bitsocial.net/developer-tools/react-hooks/) docs for client-side integration. - Read the [Bitsocial CLI](https://docs.bitsocial.net/developer-tools/cli/) docs for node and community operations. If you want to move quickly, contribute to an existing app first. If the interface, culture, or community model you want does not fit, build a separate client. ## Pick a narrow first version The best first version is not a universal social app. It is a client with one clear audience and one strong reason to exist. Good starting points include: - a cleaner imageboard client for one specific culture - a mobile-first forum client - a single-community app with strict defaults - a creator community client - a read-only discovery client - a moderation or operator console - a client optimized for a language, region, or device class Small clients are useful because Bitsocial lets them grow into the same network instead of trapping their users in a private database. ## Implementation paths There are three practical paths: 1. Fork an existing client when your idea is close to 5chan or Seedit. 2. Build a new React client with Bitsocial React hooks. 3. Build your own integration on top of node APIs and public RPC infrastructure. Public RPC should make the third path much more practical. A user can start through a hosted, non-custodial RPC provider, then move to self-hosting or a competing provider later. ## Design principle Build the client that should exist for your community, then let compatible clients compete in public. ### Decentralize All Social Media Source: https://docs.bitsocial.net/decentralize-all-social-media/ Path: docs/decentralize-all-social-media.md Description: Phase 5 of the master plan, focused on funding and building the long tail of Bitsocial social clients. # Decentralize All Social Media The end state is not one app. It is a market of clients, nodes, services, and communities that can replace platform ownership with protocol competition. Phase 5 is where Bitsocial pushes beyond the first priority categories and starts funding as many social formats as possible. ## What still needs to be built The first phases focus on the highest-leverage categories: imageboards, forums, public RPC, Bitsocial Chain, profile nodes, the Phase 3 Twitter/X alternative, and the service economy around them. After that, the network should expand into the long tail: - blogging clients - crowdfunding apps - creator video and a credible [YouTube alternative](https://github.com/bitsocialnet/wetube) - creator communities - short-form media experiments - local or language-specific social apps - niche clients that are too small for the first four phases - tools that make existing communities easier to run Some of these apps can be open source. Some can be centralized clients. Some can be built by Bitsocial Forge, and many should be built by independent developers. ## Funding many builders The goal is to make new social clients fundable before they already have platform-scale traction. That can include direct investment, grants, community funding, revenue from successful Bitsocial services, or decentralized grant programs where the broader Bitsocial community helps decide which developers should receive support. The important part is pluralism. The network should fund many attempts, not wait for one official app to cover every category. ## What success looks like Bitsocial succeeds when social media can be broken into replaceable layers: - protocol - communities - identity - hosting - discovery - moderation - monetization - apps Once those layers are replaceable, no single company has to own the entire social graph for the product to work. ## The core bet A mature Bitsocial ecosystem should contain public RPCs, self-hosted nodes, feed algorithm markets, media hosts, discovery services, open-source clients, commercial clients, and community-funded experiments. Social media finally finds its equilibrium: a fully decentralized, peer-to-peer social network that nobody owns; Bitsocial. ### Decentralize Twitter/X Source: https://docs.bitsocial.net/flagship-bitsocial-app/ Path: docs/flagship-bitsocial-app.md Description: Phase 3 of the master plan: a focused, decentralized Twitter/X alternative for text-first public conversation with replaceable infrastructure. # Decentralize Twitter/X Phase 3 is a plan to build a focused, decentralized alternative to Twitter/X. Its center is text-first public conversation: short posts, replies, reposts, follows, real-time discussion, and communities, with the platform layer broken open. Twitter/X is still defined by posts, text, and sharing ideas. The Phase 3 client should compete on that core experience and make it work exceptionally well. This page describes product direction, not a locked release spec. The exact interface, default feed, ad model, AI features, and RPC marketplace can change as the protocol and early apps mature. ## What it should prove The client should prove that a profile-based social network can avoid becoming a custodial platform: - users can own identities and profiles - communities and profile nodes can stay peer-to-peer - communities can carry network effects across Bitsocial clients - RPC providers can make the app convenient without taking custody - feed algorithms can be optional services instead of platform law - other clients can still compete for the same network The goal is to build the strongest public-conversation client possible and show how far the protocol can stretch. ## Familiar in purpose, replaceable by design The default experience should be competitive with Twitter/X at its core: a fast home feed, text posts, follows, replies, repost-style distribution, communities, notifications, search, and a ranked For You view that works immediately. Bitsocial Forge can run the first default RPC and feed service. That default can include a ranked feed and ads so the app feels complete on day one instead of asking mainstream users to assemble the whole stack themselves. The difference is that the default should not become the prison. A user should be able to switch RPCs, feeds, instances, ranking systems, ads, and discovery providers, or remove ranking entirely. The client can be opinionated at first launch while keeping every major service replaceable. That makes the client more customizable than a conventional platform. One user might keep the default ranked feed with ads. Another might use a chronological feed with no ranking. Another might choose a privacy-focused RPC, a community-run discovery service, a paid ad-free feed, or a niche algorithm built for a specific subculture. ## Cross-client communities Communities should be much more important than isolated groups inside one app. On Twitter/X, communities are confined to the product. They can be useful, but they inherit the limits of one platform, one account system, one recommendation stack, and one product surface. A Bitsocial community can be created, hosted, discovered, and used through different clients. That means the Phase 3 client can show communities and posts from the wider Bitsocial network, not only from users who started inside it. A community could have activity from an imageboard client, a Reddit-style discussion client, a niche forum client, a mobile app, and the Phase 3 client at the same time. That is the core network-effect advantage: one client can feel familiar to mainstream users while still pulling value from many clients, community nodes, RPC providers, and independent services. ## Optional feed algorithms The Phase 3 client should not force one global ranking system on everyone. Feed algorithms should be opt-in. A user could choose an algorithm from a marketplace, switch providers, use an algorithm from a company, use one run by an anonymous operator, use one built by a community, run a personal one, or use no algorithm at all. Public RPC providers are a natural place for these services to compete. They can index, rank, and recommend content, but they should not own the user or the profile. Those services can also compete on the shape of the app itself. One RPC might provide a ranked feed with ads. Another might provide an unranked chronological feed. Another might specialize in privacy, translation, moderation, community discovery, or a niche social graph. If the economics work, RPC-backed feed services could add AI features similar to what mainstream platforms are trying to put into their feeds: automatic translations, summaries, bot-assisted replies, search answers, moderation assistance, or community-note style context. Those features should be service choices, not protocol requirements. A default RPC can compete by offering a richer feed, but users and competing clients should still be able to choose simpler, private, chronological, ad-free, or community-specific alternatives. ## Non-custodial RPC Each user should be able to participate as a full peer-to-peer node through RPC without giving the RPC provider ownership over their identity or profile. The hosted path matters because most users will not start by running a server. The exit path matters just as much: a user should be able to move to their own profile node on low-spec hardware, including a Raspberry Pi, whenever they want. That is the difference between convenience and custody. ## Public conversation, strengthened by Bitsocial Chain Bitsocial Chain can bring durable naming, payments, tipping, awards, and other financial rails directly into public conversation. The Phase 3 client stays centered on posts, text, sharing ideas, and real-time discussion while sharing communities and network effects with other Bitsocial clients. ### Scale Bitsocial Economies Source: https://docs.bitsocial.net/scale-bitsocial-economies/ Path: docs/scale-bitsocial-economies.md Description: Phase 4 of the master plan, focused on funding, infrastructure pluralism, and competing services around Bitsocial. # Scale Bitsocial Economies Phase 4 is about making the ecosystem large enough that no single company, RPC, hosting provider, or client becomes the new platform owner. Bitsocial Forge can build the first public RPC and the first major services, but that should not be the end state. The network is healthier if many businesses, teams, anonymous operators, community entities, and independent developers can build useful infrastructure on top of Bitsocial. ## What needs to scale The ecosystem needs more than apps: - competing public RPCs - media hosting services - discovery and indexing services - moderation services - feed algorithm providers - profile-node hosting - analytics and observability - developer tools - grants and funding channels Each service should be replaceable. A useful provider can become successful, but users and apps should still have somewhere else to go. ## Funding as decentralization Funding should push the network toward more independent operators, not toward one permanent hub. That means funding developers who build clients, infrastructure, moderation tools, indexing tools, mobile experiences, media services, and competing RPCs. It also means leaving room for companies, community groups, anonymous operators, and solo builders to find sustainable businesses inside the network. ## Why economies matter Social infrastructure is expensive to maintain. If only one organization pays for RPC, hosting, discovery, and development, the network stays fragile even if the protocol is peer-to-peer. The stronger version is a market of services around the protocol: - clients compete for users - RPCs compete on reliability and trust model - media hosts compete on price and policy - discovery services compete on relevance - developers compete for grants, revenue, and reputation Bitsocial economies should make the protocol harder to capture by making useful work fundable across many independent actors. ### 5chan Source: https://docs.bitsocial.net/apps/5chan/ Path: docs/apps/5chan.md Description: A serverless, decentralized imageboard built on the Bitsocial protocol where anyone can create and own boards. # 5chan 5chan is a serverless, adminless, and fully decentralized imageboard that runs on the Bitsocial protocol. It follows the familiar imageboard directory structure while introducing decentralized ownership — anyone can create a board, and multiple boards can compete for the same directory slot through a voting mechanism. ## Downloads | Platform | Link | | -------- | ------------------------------------- | | Web | [5chan.app](https://5chan.app) | | Desktop | Available for Mac, Windows, and Linux | | Mobile | Available for Android | ## How boards work 5chan organizes content into boards using a classic directory layout (e.g., `/b/`, `/g/`). Unlike traditional imageboards where a central admin controls every board, 5chan allows any user to create and fully own their own board. When multiple boards target the same directory slot, they compete for that position through voting. ### Creating a board To create a new board, you need to run `bitsocial-cli` as a peer-to-peer node. This ensures your board is hosted in a decentralized manner without relying on any central server. ### Directory assignments Directory slot assignments (which board appears at which path) are currently managed through GitHub pull requests to the `5chan-directories.json` file. This is a temporary process — future releases will support in-app board creation and pubsub-based voting to handle directory assignments automatically. ## Internals Under the hood, 5chan uses the shared Bitsocial protocol client layer for its network interactions. The web app at 5chan.app runs a Helia node in the browser by default, so a normal tab joins the network as a peer: it loads boards from other peers and publishes over pubsub, with no centralized IPFS gateway in the content path. See [Browser Peer-to-Peer](https://docs.bitsocial.net/browser-p2p/) for what that involves and what a browser node still cannot do. ## Links - **GitHub**: [github.com/bitsocialnet/5chan](https://github.com/bitsocialnet/5chan) - **Telegram**: [t.me/fivechandev](https://t.me/fivechandev) - **License**: GPL-2.0-only ### Seedit Source: https://docs.bitsocial.net/apps/seedit/ Path: docs/apps/seedit.md Description: A serverless, decentralized Reddit-style forum built on the Bitsocial protocol where anyone can create and own communities. # Seedit Seedit is a serverless, adminless, and fully decentralized alternative to old-style Reddit, built on the Bitsocial protocol. It gives users complete ownership over their communities — anyone can create one, and multiple communities can compete for default listing slots. ## Downloads | Platform | Link | | --------- | ------------------------------------------------ | | Web | [seedit.app](https://seedit.app) | | Web (ENS) | `seedit.eth` via Brave browser or IPFS Companion | | Desktop | Available for Mac, Windows, and Linux | | Mobile | Available for Android | ## How communities work Seedit organizes content into communities similar to subreddits. Each community is owned by its creator and operates without centralized administration. When multiple communities target the same default slot, they can compete for that position. ### Creating a community There are two ways to create a new community: 1. **Desktop client** — Use the built-in GUI in the desktop application to set up and configure your community. 2. **bitsocial-cli** — Run the CLI tool to create and manage communities from the command line. ### Default community list The list of default communities that appear on the front page is defined in `default-multisub.json` within the lists repository. To add your community to the defaults, submit a pull request with your community's details. ## Links - **GitHub**: [github.com/bitsocialnet/seedit](https://github.com/bitsocialnet/seedit) - **Telegram**: [t.me/seeditreact](https://t.me/seeditreact) - **License**: GPL-2.0-only ### Bitsocial CLI Source: https://docs.bitsocial.net/developer-tools/cli/ Path: docs/developer-tools/cli.md Description: Command-line interface for running a Bitsocial node, creating communities, and managing protocol operations. # Bitsocial CLI The `bitsocial-cli` is a command-line tool for interacting with the Bitsocial protocol backend. It lets you run a local P2P daemon, create and configure communities, and publish content -- all from the terminal. It is built on top of the shared Bitsocial protocol client layer and is used by [5chan](https://docs.bitsocial.net/apps/5chan/) and [Seedit](https://docs.bitsocial.net/apps/seedit/) for community creation and node management. ## Installation Pre-built binaries are available for Windows, macOS, and Linux. Download the latest release for your platform from GitHub: **[Download from GitHub Releases](https://github.com/bitsocialnet/bitsocial-cli/releases)** After downloading, make the binary executable (macOS/Linux): ```bash chmod +x bitsocial ``` ## Running the Daemon The most common use of the CLI is running a Bitsocial node. The daemon starts the P2P networking layer and exposes a local API that clients can connect to. ```bash bitsocial daemon ``` On first launch, the daemon outputs links to the **WebUI**, a browser-based graphical interface for managing your node, communities, and settings. This is useful if you prefer a GUI over terminal commands. ## Key Actions | Action | Description | | ---------------------- | ----------------------------------------------------- | | Start the daemon | Launch the Bitsocial P2P node | | Create a community | Create a new community | | Edit a community | Update community settings (title, description, rules) | | List local communities | List communities hosted on this node | | Start a community | Start serving a specific community | | Stop a community | Stop serving a specific community | Run the CLI with `--help` to see the current command names and flags exposed by your installed release: ```bash bitsocial --help bitsocial daemon --help ``` ## Typical Workflow A common setup flow for hosting a new community: ```bash # 1. Start the daemon bitsocial daemon # 2. In another terminal, inspect the available community-management commands bitsocial --help ``` From there, use the installed release's community-management commands to create, configure, and start serving a community. Once started, the community is live on the Bitsocial network and accessible from compatible clients. ## Links - **GitHub:** [bitsocialnet/bitsocial-cli](https://github.com/bitsocialnet/bitsocial-cli) ### React Hooks Source: https://docs.bitsocial.net/developer-tools/react-hooks/ Path: docs/developer-tools/react-hooks.md Description: React hooks library for building decentralized social applications on the Bitsocial protocol. # React Hooks The `bitsocial-react-hooks` package provides a familiar React hooks API for interacting with the Bitsocial protocol. It handles fetching feeds, comments, and author profiles, managing accounts, publishing content, and subscribing to communities -- all without relying on a central server. This library is the primary interface used by [5chan](https://docs.bitsocial.net/apps/5chan/) and other Bitsocial client applications. `bitsocial-react-hooks` is currently consumed directly from GitHub rather than published to npm. ## Installation Because the package is not yet on npm, install it directly from GitHub, pinning to a specific commit hash: ```bash yarn add https://github.com/bitsocialnet/bitsocial-react-hooks.git# ``` Replace `` with the commit you want to target. ## API Overview The hooks are organized into functional categories. Below is a summary of the most commonly used hooks in each category. For complete signatures, parameters, and return types, see the [full API reference on GitHub](https://github.com/bitsocialnet/bitsocial-react-hooks). ### Accounts Manage local user accounts, identity, and settings. - `useAccount(accountName?)` -- returns the active (or named) account object - `useAccounts()` -- returns all locally stored accounts - `useAccountComments(options?)` -- returns comments published by the active account ### Comments Fetch and interact with individual comments and threads. - `useComment(commentCid?)` -- fetches a single comment by its CID - `useComments(commentCids?)` -- fetches multiple comments in batch - `useEditedComment(comment?)` -- returns the latest edited version of a comment ### Communities Retrieve community metadata and settings. - Single-community lookup hook -- fetches a community by address - Multi-community lookup hook -- fetches multiple communities - Community stats hook -- returns subscriber and post counts ### Authors Look up author profiles and metadata. - `useAuthor(authorAddress?)` -- fetches an author profile - `useAuthorComments(options?)` -- returns comments by a specific author - `useResolvedAuthorAddress(authorAddress?)` -- resolves a human-readable address (e.g., ENS) to its protocol address ### Feeds Subscribe to and paginate content feeds. - `useFeed(options?)` -- returns a paginated feed of posts from one or more communities - `useBufferedFeeds(feedOptions?)` -- pre-buffers multiple feeds for faster rendering - `useAuthorFeed(authorAddress?)` -- returns a feed of posts by a specific author ### Actions Publish content and perform write operations. - `usePublishComment(options?)` -- publish a new comment or reply - `usePublishVote(options?)` -- cast an upvote or downvote - `useSubscribe(options?)` -- subscribe or unsubscribe from a community ### States and RPC Monitor connection state and interact with a remote Bitsocial daemon. - `useClientsStates(options?)` -- returns the connection state of IPFS/pubsub clients - RPC settings hook -- returns current RPC daemon configuration ## Development To work on the hooks library locally: **Prerequisites:** Node.js, Corepack enabled, Yarn 4 ```bash git clone https://github.com/bitsocialnet/bitsocial-react-hooks.git cd bitsocial-react-hooks corepack enable yarn install ``` Refer to the repository README for test and build commands. ## Links - **GitHub:** [bitsocialnet/bitsocial-react-hooks](https://github.com/bitsocialnet/bitsocial-react-hooks) - **License:** GPL-2.0-only ### Captcha Canvas Challenge Source: https://docs.bitsocial.net/anti-spam/captcha-canvas-challenge/ Path: docs/anti-spam/captcha-canvas-challenge.md Description: Standalone image-based captcha challenge for Bitsocial communities. # Captcha Canvas Challenge Captcha Canvas Challenge is a standalone image captcha package for Bitsocial communities. It renders randomized text onto a canvas and lets a community ask authors to solve the image before a publication is accepted. - **Source code and current README:** [github.com/bitsocialnet/captcha-canvas-challenge](https://github.com/bitsocialnet/captcha-canvas-challenge#readme) - **npm package:** [`@bitsocial/captcha-canvas-challenge`](https://www.npmjs.com/package/@bitsocial/captcha-canvas-challenge) ## Installation ```bash npm install @bitsocial/captcha-canvas-challenge ``` ## Where It Fits Captcha challenges are useful when a community wants a simple interactive gate for low-stakes spam resistance. This package is intentionally narrow: it provides the challenge implementation, while the community or Bitsocial node decides when and how to present it. For stronger protection, combine it with broader moderation or risk-scoring systems instead of treating a captcha as a complete anti-spam strategy. ## Current Package Reference This page is intentionally an overview, not a mirrored setup guide. The package README is the source of truth for current challenge names, registration examples, CLI examples, supported options, requirements, and security notes: - [Captcha Canvas Challenge README](https://github.com/bitsocialnet/captcha-canvas-challenge#readme) Prefer the upstream README when configuring a live community, because package options and install flows are versioned with that package rather than this website. ### EVM Contract Call Challenge Source: https://docs.bitsocial.net/anti-spam/evm-contract-call/ Path: docs/anti-spam/evm-contract-call.md Description: Anti-spam challenge that verifies on-chain conditions by calling an EVM smart contract. # EVM Contract Call Challenge EVM Contract Call Challenge verifies an author's on-chain state before allowing a publication. Community owners can require a wallet or resolved identity to satisfy a read-only smart-contract condition, such as holding a minimum token balance, before posting. - **Source code and current README:** [github.com/bitsocialnet/evm-contract-challenge](https://github.com/bitsocialnet/evm-contract-challenge#readme) - **npm package:** [`@bitsocial/evm-contract-challenge`](https://www.npmjs.com/package/@bitsocial/evm-contract-challenge) ## Installation ```bash npm install @bitsocial/evm-contract-challenge ``` ## Where It Fits Use this challenge for communities where participation should depend on an external EVM signal: token ownership, NFT ownership, proof-of-personhood scores, governance membership, or another contract-readable condition. The challenge is automatic from the author's perspective once configured. It checks eligible wallet or identity sources, calls the configured contract method, and compares the returned value against the community's condition. ## Current Package Reference This page is intentionally an overview, not a mirrored configuration reference. The package README is the source of truth for challenge names, Bitsocial CLI examples, pkc-js registration, option defaults, ABI examples, RPC behavior, and supported wallet sources: - [EVM Contract Challenge README](https://github.com/bitsocialnet/evm-contract-challenge#readme) Prefer the upstream README when configuring a live community, because contract options and examples are versioned with that package rather than this website. ## When to Use It EVM Contract Call Challenge is ideal for: - **Token-gated communities** that restrict posting to token holders. - **NFT-gated access** where ownership of a specific NFT is required. - **DAO governance spaces** where participation is limited to governance token holders. For communities that do not rely on on-chain identity, consider [Spam Blocker](https://docs.bitsocial.net/anti-spam/spam-blocker/) or [Voucher Challenge](https://docs.bitsocial.net/anti-spam/voucher-challenge/) instead. ### Spam Blocker Source: https://docs.bitsocial.net/anti-spam/spam-blocker/ Path: docs/anti-spam/spam-blocker.md Description: Centralized spam detection service with risk scoring, OAuth challenges, and configurable tier thresholds. # Spam Blocker Spam Blocker is a centralized spam detection service that evaluates incoming publications and assigns risk scores. It consists of two packages: - **`@bitsocial/spam-blocker-server`** -- the HTTP server that hosts the evaluation and challenge APIs. - **`@bitsocial/spam-blocker-challenge`** -- a lightweight client package that communities integrate to send publications for evaluation. **Source code:** [github.com/bitsocialnet/spam-blocker](https://github.com/bitsocialnet/spam-blocker) ## How Risk Scoring Works Every publication submitted to the `/evaluate` endpoint receives a numeric risk score. The score is a weighted combination of several signals: | Signal | Description | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Account age | Newer accounts receive higher risk scores. | | Karma | Accumulated community karma reduces risk. | | Author reputation | Reputation data gathered by the background network indexer. | | Content analysis | Text-level heuristics (link density, known spam patterns, etc.). | | Velocity | Rapid successive posts from the same author increase risk. | | IP intelligence | Country-level geolocation and threat-feed lookups. Only country codes are stored -- raw IP addresses are never shared with communities. | ## Tier Thresholds The risk score maps to one of four configurable tiers that determine what happens next: 1. **Auto-accept** -- score is low enough that the publication is approved without any challenge. 2. **OAuth-sufficient** -- the author must complete an OAuth verification to proceed. 3. **OAuth-plus-more** -- OAuth alone is not enough; additional verification (e.g., CAPTCHA) is required. 4. **Auto-reject** -- score is too high; the publication is rejected outright. All threshold values are configurable per community. ## Challenge Flow When a publication falls into a tier that requires verification, the challenge flow begins: 1. The author is first prompted to authenticate via **OAuth** (GitHub, Google, Twitter, and other supported providers). 2. If OAuth alone is insufficient (tier 3), a **CAPTCHA fallback** powered by Cloudflare Turnstile is presented. 3. The OAuth identity is used solely for verification -- it is **never shared** with the community or other users. ## API Endpoints ### `POST /evaluate` Submit a publication for risk evaluation. Returns the computed risk score and the required challenge tier. ### `POST /challenge/verify` Submit the result of a completed challenge (OAuth token, CAPTCHA solution, or both) for verification. ### `GET /iframe/:sessionId` Returns an embeddable HTML page that renders the appropriate challenge UI for the given session. ## Rate Limiting Rate limits are applied dynamically based on author age and reputation. Newer or lower-reputation authors face stricter limits, while established authors enjoy more generous thresholds. This prevents spam floods without penalizing trusted participants. ## Background Network Indexer The server runs a background indexer that continuously crawls the network to build and maintain author reputation data. This data feeds directly into the risk scoring pipeline, allowing the system to recognize repeat good-faith participants across communities. ## Privacy Spam Blocker is designed with privacy in mind: - OAuth identities are used only for challenge verification and are **never disclosed** to communities. - IP addresses are resolved to **country codes only**; raw IPs are not stored or shared. ## Database The server uses **SQLite** (via `better-sqlite3`) for local persistence of reputation data, session state, and configuration. ### Voucher Challenge Source: https://docs.bitsocial.net/anti-spam/voucher-challenge/ Path: docs/anti-spam/voucher-challenge.md Description: Anti-spam challenge that gates publishing behind unique voucher codes distributed by community owners. # Voucher Challenge Voucher Challenge gates content publication behind unique voucher codes distributed by the community owner. Rather than relying on automated scoring, it shifts trust to a manual invite flow where known people receive codes through a channel the owner controls. - **Source code and current README:** [github.com/bitsocialnet/voucher-challenge](https://github.com/bitsocialnet/voucher-challenge#readme) - **npm package:** [`@bitsocial/voucher-challenge`](https://www.npmjs.com/package/@bitsocial/voucher-challenge) ## Installation ```bash npm install @bitsocial/voucher-challenge ``` ## How It Works 1. A community owner generates one or more unique voucher codes. 2. The owner distributes those codes to trusted authors through a channel of their choice (direct message, email, in-person, etc.). 3. When an author attempts to publish, the challenge system prompts them for a voucher code. 4. The code is validated -- if it is genuine and has not already been used, the publication is accepted. Each voucher code is tied to a specific author once redeemed, preventing reuse by others. ## Current Package Reference This page is intentionally an overview, not a mirrored setup guide. The package README is the source of truth for current challenge names, Bitsocial CLI examples, pkc-js registration, supported options, and redemption behavior: - [Voucher Challenge README](https://github.com/bitsocialnet/voucher-challenge#readme) Prefer the upstream README when configuring a live community, because voucher options and install flows are versioned with that package rather than this website. ## When to Use It Voucher Challenge is best suited for: - **Invite-only communities** where membership is intentionally restricted. - **Curated spaces** where the owner personally vets every participant. - **High-trust environments** where automated spam scoring is unnecessary or undesirable. Because it requires manual code distribution, it does not scale to large open communities. For those scenarios, consider [Spam Blocker](https://docs.bitsocial.net/anti-spam/spam-blocker/) or [EVM Contract Call Challenge](https://docs.bitsocial.net/anti-spam/evm-contract-call/) instead. ### BSO Resolver Source: https://docs.bitsocial.net/infrastructure/bso-resolver/ Path: docs/infrastructure/bso-resolver.md Description: Resolve .bso domain names to public keys via Bitsocial TXT records. # BSO Resolver BSO Resolver translates `.bso` domain names into their corresponding public keys by reading Bitsocial TXT records. It is the resolver package used by Bitsocial tooling when a user-facing `.bso` name needs to become the key material understood by the peer-to-peer stack. - **Source code and current README:** [github.com/bitsocialnet/bso-resolver](https://github.com/bitsocialnet/bso-resolver#readme) - **npm package:** [`@bitsocial/bso-resolver`](https://www.npmjs.com/package/@bitsocial/bso-resolver) ## Installation ```bash npm install @bitsocial/bso-resolver ``` ## Where It Fits Bitsocial names are meant to be human-readable entry points for communities and authors. The resolver keeps that naming layer separate from application code, so clients can ask whether a name is supported and then resolve it through the package's runtime-specific entry point. Use it when you are integrating a Bitsocial-aware client, command-line tool, or service that needs to accept `.bso` names instead of only raw public keys. ## Current Package Reference This page is intentionally an overview, not a mirrored API reference. The package README is the source of truth for constructor options, return types, caching behavior, entry points, provider examples, and supported shutdown semantics: - [BSO Resolver README](https://github.com/bitsocialnet/bso-resolver#readme) Prefer the upstream README when copying code into a project, because resolver behavior is versioned with that package rather than this website. ### Mintpass Source: https://docs.bitsocial.net/infrastructure/mintpass/ Path: docs/infrastructure/mintpass.md Description: NFT-based authentication system that helps Bitsocial communities verify users and reduce sybil attacks. # Mintpass Mintpass is an NFT-based authentication system for Bitsocial communities. Users mint a non-transferable verification NFT after completing a challenge (such as SMS OTP), and communities can check NFT ownership to guard against sybil attacks like fake votes, ban evasion, and spam. - **GitHub**: [bitsocialnet/mintpass](https://github.com/bitsocialnet/mintpass) - **License**: MIT ## How It Works The verification flow has four steps: 1. **Request** -- The user visits `mintpass.org/request` to begin the process. 2. **Challenge** -- The user completes an SMS one-time-password verification. 3. **Mint** -- Upon successful verification, a non-transferable NFT is minted to the user's wallet. 4. **Verify** -- Communities query NFT ownership to confirm the user has been verified. Because the NFT is non-transferable, it stays bound to the wallet that completed verification, preventing users from trading or selling their verified status. ## Project Structure The repository is organized into three main areas: | Directory | Purpose | | ------------ | ----------------------------------------------------- | | `contracts/` | Solidity smart contracts for the verification NFT. | | `challenge/` | Integration layer for the Bitsocial challenge system. | | `web/` | Next.js and React frontend for the minting flow. | ## Privacy and Data Handling Mintpass takes a minimal-data approach: - **Operational data** (OTP codes, session tokens) is stored in Redis with short TTLs and automatically expires. - **Mint association** (the link between a verified identity and a wallet) is the only persistent record. No phone numbers or personal details are retained after the verification window closes. ## Optional Security Layers Community operators can enable additional protections depending on their threat model: - **IP reputation checks** -- Score incoming requests against known abuse databases. - **Phone-risk assessment** -- Flag disposable or VoIP numbers before issuing a challenge. - **Geoblocking** -- Restrict verification to specific regions. - **Per-IP cooldowns** -- Rate-limit repeated verification attempts from the same address. ## Technology Stack | Layer | Technology | | --------- | ------------------------------------------- | | Contracts | Solidity, deployed with Hardhat and Foundry | | Frontend | Next.js + React | | Network | Base (Ethereum L2) | Deploying on Base keeps gas costs low while inheriting Ethereum's security guarantees. ## Roadmap Planned improvements include: - **Pay-to-mint option** -- Allow communities to require a small fee for minting, adding an economic sybil barrier. - **Additional verification signals** -- Expand beyond SMS to other identity signals. - **Expanded admin tooling** -- Richer dashboards and controls for community operators. ### Pubsub Provider Source: https://docs.bitsocial.net/infrastructure/pubsub-provider/ Path: docs/infrastructure/pubsub-provider.md Description: Fallback pubsub relay and delegated routing provider for Bitsocial operators. # Pubsub Provider Pubsub Provider is an operator service for running a Bitsocial-compatible pubsub fallback relay with a bundled Kubo node. Modern Bitsocial clients such as 5chan and Seedit use pure peer-to-peer networking in the browser by default, but this service remains useful as an optional fallback path for users who disable browser P2P or for operators who want public compatibility endpoints. - **GitHub**: [bitsocialnet/pubsub-provider](https://github.com/bitsocialnet/pubsub-provider) - **Docker image**: [`ghcr.io/bitsocialnet/pubsub-provider`](https://github.com/bitsocialnet/pubsub-provider/pkgs/container/pubsub-provider) - **License**: GPL-3.0-or-later ## What It Runs - a public HTTP proxy for pubsub, gateway, name-provider, and delegated routing routes - a bundled Kubo node with pubsub enabled - a delegated HTTP routing provider at `/routing/v1/providers` - Prometheus metrics at `/metrics` - optional basic-auth access to the full Kubo RPC API ## Ports The defaults are chosen so Pubsub Provider can run next to [Bitsocial Seeder](https://bitsocial.net/apps/bitsocial-seeder/) on the same VPS without a swarm-port conflict. | Purpose | Default | Notes | | ----------------- | ---------------------------- | ---------------------------------------------------- | | Public HTTP proxy | `8000` app, `80` Docker host | Set `PUBSUB_PROVIDER_HTTP_PORT` to change host port. | | Kubo swarm | `4002` TCP/UDP | Avoids the seeder's default Kubo swarm port `4001`. | | Kubo API | `5001` local only | Used internally by the proxy. | | Kubo gateway | `8080` local only | Used internally by the proxy. | ## Docker Setup ```bash git clone https://github.com/bitsocialnet/pubsub-provider.git cd pubsub-provider docker compose pull docker compose up -d ``` Check logs: ```bash docker logs --follow pubsub-provider ``` Test the proxy: ```bash curl http://127.0.0.1/commit-hash ``` ## Upgrading If you previously ran the old `latest` image, force Compose to recreate the container from the pinned published image: ```bash git pull docker compose down docker compose pull docker compose up -d --force-recreate ``` Verify the fixed image is running: ```bash docker inspect pubsub-provider --format 'image={{.Image}} restarts={{.RestartCount}}' docker exec pubsub-provider /app/bin/ipfs version curl http://127.0.0.1/commit-hash ``` The logs should include `using Kubo binary at /app/bin/ipfs` and should not include `downloading ipfs`. ## Running With Bitsocial Seeder If the same host also runs `bitsocial-seeder`, keep Pubsub Provider on swarm port `4002` or another non-`4001` port: ```bash PUBSUB_PROVIDER_SWARM_PORT=4002 docker compose up -d ``` This avoids the port conflict that happens when two Kubo nodes both try to bind TCP/UDP `4001`. ## Configuration Common environment overrides: ```env PUBSUB_PROVIDER_HTTP_PORT=80 PUBSUB_PROVIDER_SWARM_PORT=4002 PUBSUB_PROVIDER_PORTS=8000 KUBO_RPC_URL=http://127.0.0.1:5001/api/v0 IPFS_GATEWAY_URL=http://127.0.0.1:8080 HTTP_ROUTER_URLS=https://example-router.invalid PUBSUB_PROVIDER_ROUTING_STORE_PATH= BASIC_AUTH_USERNAME= BASIC_AUTH_PASSWORD= IPFS_GATEWAY_USE_SUBDOMAINS=false SHUTDOWN_KEY= ETH_PROVIDER_URL= ETH_PROVIDER_URL_WS= SOL_PROVIDER_URL= ``` Use the provider as a fallback relay, not as a replacement for browser P2P. Keep running the tracker infrastructure separately when the network needs dedicated peer discovery capacity. ### Telegram Bots Source: https://docs.bitsocial.net/infrastructure/telegram-bots/ Path: docs/infrastructure/telegram-bots.md Description: Feed bots that monitor Bitsocial community lists and forward posts to Telegram channels. # Telegram Bots The Bitsocial Telegram bots monitor client community lists on the Bitsocial network and automatically forward new posts into Telegram channels. Each forwarded message includes inline buttons that link back to the original post on 5chan and Seedit. - **GitHub**: [bitsocialnet/bitsocial-telegram-bots](https://github.com/bitsocialnet/bitsocial-telegram-bots) ## Available Bots | Bot | Status | Description | | --------------- | ------- | ------------------------------------------------------------------ | | **5chan Feed** | Active | Monitors all 5chan directories and forwards new posts to Telegram. | | **Seedit Feed** | Planned | Will provide the same functionality for Seedit communities. | ## Setup ### Prerequisites - Node.js - Yarn - A Telegram bot token (create one via [BotFather](https://t.me/BotFather)) ### Installation Clone the repository and install dependencies: ```bash git clone https://github.com/bitsocialnet/bitsocial-telegram-bots.git cd bitsocial-telegram-bots yarn install ``` ### Configuration Create a `.env` file in the project root with your bot token: ```env BOT_TOKEN=your_telegram_bot_token ``` ### Running Start the bot after configuring your environment: ```bash yarn start ``` ## Post Format When the bot forwards a post to Telegram, it includes two inline buttons: - **View on 5chan** -- Opens the post in the 5chan web client. - **View on Seedit** -- Opens the post in the Seedit web client. This lets Telegram subscribers jump directly to the full discussion thread on whichever client they prefer. ### Bug Investigation Workflow Source: https://docs.bitsocial.net/agent-playbooks/bug-investigation/ Path: docs/agent-playbooks/bug-investigation.md Description: Use this when a bug is reported in a specific file/line/code block. # Bug Investigation Workflow Use this when a bug is reported in a specific file/line/code block. ## Mandatory First Step Before editing, check git history for the relevant code. Previous contributors may have introduced behavior for an edge case/workaround. ## Workflow 1. Scan recent commit titles (titles only) for the file/area: ```bash # Recent commit titles for a specific file git log --oneline -10 -- about/src/components/post-desktop/post-desktop.tsx # Recent commit titles for a specific line range git blame -L 120,135 about/src/components/post-desktop/post-desktop.tsx ``` 2. Inspect only relevant commits with scoped diffs: ```bash # Show commit message + diff for one file git show -- path/to/file.tsx ``` 3. Continue with reproduction and fix after understanding the history context. ## Troubleshooting Rule When blocked, search the web for recent fixes/workarounds. ### Commit and Issue Format Source: https://docs.bitsocial.net/agent-playbooks/commit-issue-format/ Path: docs/agent-playbooks/commit-issue-format.md Description: Use this when proposing or implementing meaningful code changes. # Commit and Issue Format Use this when proposing or implementing meaningful code changes. ## Commit Suggestion Format - **Title:** Conventional Commits style, short, wrapped in backticks. - Use `perf` (not `fix`) for performance optimizations. - **Description:** Optional 2-3 informal sentences describing the solution. Concise, technical, no bullet points. Example: > **Commit title:** `fix: correct date formatting in timezone conversion` > > Updated `formatDate()` in `date-utils.ts` to properly handle timezone offsets. ## GitHub Issue Suggestion Format - **Title:** As short as possible, wrapped in backticks. - **Description:** 2-3 informal sentences describing the problem (not the solution), as if still unresolved. Example: > **GitHub issue:** > > - **Title:** `Date formatting displays incorrect timezone` > - **Description:** Comment timestamps show incorrect timezones when users view posts from different regions. The `formatDate()` function doesn't account for user's local timezone settings. ### Agent Hooks Setup Source: https://docs.bitsocial.net/agent-playbooks/hooks-setup/ Path: docs/agent-playbooks/hooks-setup.md Description: If your AI coding assistant supports lifecycle hooks, configure these for this repo. # Agent Hooks Setup If your AI coding assistant supports lifecycle hooks, configure these for this repo. ## Recommended Hooks | Hook | Command | Purpose | | --------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `afterFileEdit` | `scripts/agent-hooks/format.sh` | Auto-format files after AI edits | | `afterFileEdit` | `scripts/agent-hooks/yarn-install.sh` | Run `corepack yarn install` when `package.json` changes | | `afterFileEdit` | `scripts/agent-hooks/react-pattern-review.sh` | When a diff adds `useEffect`/memo primitives in `about/src/`, remind the agent to reconsider with the React review skills | | `stop` | `scripts/agent-hooks/sync-git-branches.sh` | Prune stale refs and delete integrated temporary task branches | | `stop` | `scripts/agent-hooks/react-pattern-review.sh` | Re-scan the current diff for new React effects/memos in `about/src/` before the final verify gate | | `stop` | `scripts/agent-hooks/verify.sh` | Hard-gate targeted build verification, lint, typecheck, and format checks; keep `yarn npm audit` informational and run `yarn knip` separately as an advisory audit when dependencies/imports change | ## Why - Consistent formatting - Lockfile stays in sync - New `useEffect`/memo additions in the about site get an explicit second look before the agent finishes - Workspace-relevant build/lint/type issues caught early without forcing the full multi-locale docs build on every task - Security visibility via `yarn npm audit` - Dependency/import drift can be checked with `yarn knip` without turning it into a noisy global stop hook - One shared hook implementation for both Codex and Cursor - Temporary task branches stay aligned with the repo's worktree workflow ## Example Hook Scripts ### Format Hook ```bash #!/bin/bash # Auto-format JS/TS files after AI edits # Hook receives JSON via stdin with file_path input=$(cat) file_path=$(echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:.*"\([^"]*\)"/\1/') case "$file_path" in *.js|*.jsx|*.ts|*.tsx|*.mjs|*.cjs|*.json|*.css) corepack yarn exec oxfmt "$file_path" 2>/dev/null ;; esac exit 0 ``` ### Verify Hook ```bash #!/bin/bash # Run targeted build verification, lint, typecheck, format check, and security audit when agent finishes cat > /dev/null # consume stdin status=0 corepack yarn build:verify || status=1 corepack yarn lint || status=1 corepack yarn typecheck || status=1 corepack yarn format:check || status=1 echo "=== yarn npm audit ===" && (corepack yarn npm audit || true) # informational exit $status ``` By default, `scripts/agent-hooks/verify.sh` exits non-zero when a required check fails. Set `AGENT_VERIFY_MODE=advisory` only when you intentionally need signal from a broken tree without blocking the hook. Keep `yarn knip` out of the hard gate unless the repo explicitly decides to fail on advisory import/dependency issues. Lifecycle hooks do not replace manual browser verification. For UI or visual changes, still run `playwright-cli` checks across `chrome`, `firefox`, and `webkit`, plus a mobile viewport flow in each engine when responsiveness or touch behavior changed. ### Yarn Install Hook ```bash #!/bin/bash # Run corepack yarn install when package.json is changed # Hook receives JSON via stdin with file_path input=$(cat) file_path=$(echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:.*"\([^"]*\)"/\1/') if [ -z "$file_path" ]; then exit 0 fi if [ "$file_path" = "package.json" ]; then cd "$(dirname "$0")/../.." || exit 0 echo "package.json changed - running corepack yarn install to update yarn.lock..." corepack yarn install fi exit 0 ``` Configure hook wiring according to your agent tool docs (`hooks.json`, equivalent, etc.). In this repo, `.codex/hooks/*.sh` and `.cursor/hooks/*.sh` should stay as thin wrappers that delegate to the shared implementations under `scripts/agent-hooks/`. ### Known Surprises Source: https://docs.bitsocial.net/agent-playbooks/known-surprises/ Path: docs/agent-playbooks/known-surprises.md Description: This file tracks repository specific confusion points that caused agent mistakes. # Known Surprises This file tracks repository-specific confusion points that caused agent mistakes. ## Entry Criteria Add an entry only if all are true: - It is specific to this repository (not generic advice). - It is likely to recur for future agents. - It has a concrete mitigation that can be followed. If uncertain, ask the developer before adding an entry. ## Entry Template ```md ### [Short title] - **Date:** YYYY-MM-DD - **Observed by:** agent name or contributor - **Context:** where/when it happened - **What was surprising:** concrete unexpected behavior - **Impact:** what went wrong or could go wrong - **Mitigation:** exact step future agents should take - **Status:** confirmed | superseded ``` ## Entries ### Vercel app production domains can drift back to Git master deployments - **Date:** 2026-04-28 - **Observed by:** Tommaso + Codex - **Context:** Verifying Seedit and 5chan app mirrors in the Bitsocial Web app directory. - **What was surprising:** The Vercel `seedit` and `5chan` projects had `gitProviderOptions.createDeployments = "enabled"`, so GitHub `master` pushes were promoted to the production domains even though the repo policy expects production app mirrors to serve release artifacts only. - **Impact:** App-directory verified mirror badges can become false because production domains serve the latest development commit instead of the GitHub release ZIP whose `index.html` hash is recorded in `about/src/lib/apps-data.ts`. - **Mitigation:** Before adding or refreshing mirror verification metadata, check the Vercel project with `vercel api /v9/projects/` and confirm `gitProviderOptions.createDeployments = "disabled"`. Deploy release ZIP contents with `vercel deploy --prebuilt --prod` and use `seedit-omega.vercel.app` or `5chan-omega.vercel.app` for development deployments. - **Status:** confirmed ### Portless 0.11 reuses legacy proxy state unless the launcher forces HTTPS - **Date:** 2026-04-28 - **Observed by:** Tommaso + Codex - **Context:** Upgrading the normal `yarn start` flow from the old `http://bitsocial.localhost:1355` proxy URL to `https://bitsocial.localhost`. - **What was surprising:** Even with `portless@0.11.1` installed, Portless reused the existing `~/.portless/proxy.port = 1355` HTTP proxy and printed the legacy `:1355` URL. - **Impact:** Updating package versions and docs is not enough; `yarn start` can still advertise and use the old URL when a contributor has legacy Portless state running. - **Mitigation:** Keep the start scripts explicitly starting the Portless HTTPS proxy on port `443` before registering app routes, so the runtime flow migrates away from persisted `1355` state instead of inheriting it. - **Status:** confirmed ### Portless changes the canonical local app URL - **Date:** 2026-03-18 - **Observed by:** Codex - **Context:** Browser verification and smoke flows - **What was surprising:** The default local URL is not the usual Vite port. The repo expects `https://bitsocial.localhost` through Portless, so checking `localhost:3000` or `localhost:5173` can hit the wrong app or nothing at all. - **Impact:** Browser checks can fail or validate the wrong target even when the dev server is healthy. - **Mitigation:** Use `https://bitsocial.localhost` first. Only bypass it with `PORTLESS=0 corepack yarn start` when you explicitly need a direct Vite port. - **Status:** confirmed ### Commitizen hooks block non-interactive commits - **Date:** 2026-03-18 - **Observed by:** Codex - **Context:** Agent-driven commit workflows - **What was surprising:** `git commit` triggers Commitizen through Husky and waits for interactive TTY input, which hangs non-interactive agent shells. - **Impact:** Agents can stall indefinitely during what should be a normal commit. - **Mitigation:** Use `git commit --no-verify -m "message"` for agent-created commits. Humans can still use `corepack yarn commit` or `corepack yarn exec cz`. - **Status:** confirmed ### Corepack is required to avoid Yarn classic - **Date:** 2026-03-19 - **Observed by:** Codex - **Context:** Package manager migration to Yarn 4 - **What was surprising:** The machine still has a global Yarn classic install on `PATH`, so running plain `yarn` can resolve to v1 instead of the pinned Yarn 4 version. - **Impact:** Developers can accidentally bypass the repo's package-manager pinning and get different install behavior or lockfile output. - **Mitigation:** Use `corepack yarn ...` for shell commands, or run `corepack enable` first so plain `yarn` resolves to the pinned Yarn 4 version. - **Status:** confirmed ### Fixed Portless app names collide across Bitsocial Web worktrees - **Date:** 2026-03-30 - **Observed by:** Codex - **Context:** Starting `yarn start` in one Bitsocial Web worktree while another worktree was already serving through Portless - **What was surprising:** Using the literal Portless app name `bitsocial` in every worktree makes the route itself collide, even when the backing ports are different, so the second process fails because `bitsocial.localhost` is already registered. - **Impact:** Parallel Bitsocial Web branches can block each other even though Portless is meant to let them coexist safely. - **Mitigation:** Keep Portless startup behind `scripts/start-dev.mjs`, which now uses a branch-scoped `*.bitsocial.localhost` route outside the canonical case and falls back to a branch-scoped route when the bare `bitsocial.localhost` name is already occupied. - **Status:** confirmed ### Docs preview used to hard-code port 3001 - **Date:** 2026-03-30 - **Observed by:** Codex - **Context:** Running `yarn start` alongside other local repos and agents - **What was surprising:** The root dev command ran the docs workspace with `docusaurus start --port 3001`, so the whole dev session failed whenever another process already owned `3001`, even though the main app already used Portless. - **Impact:** `yarn start` could kill the web process immediately after it booted, interrupting unrelated local work over a docs-port collision. - **Mitigation:** Keep docs startup behind `yarn start:docs`, which now uses Portless plus `scripts/start-docs.mjs` to honor an injected free port or fall back to the next available port when run directly. - **Status:** confirmed ### Fixed docs Portless hostname was hard-coded - **Date:** 2026-04-03 - **Observed by:** Codex - **Context:** Running `yarn start` in a secondary Bitsocial Web worktree while another worktree was already serving docs through Portless - **What was surprising:** `start:docs` still registered the literal `docs.bitsocial.localhost` hostname, so `yarn start` could fail even though the about app already knew how to avoid Portless route collisions for its own hostname. - **Impact:** Parallel worktrees could not reliably use the root dev command because the docs process exited first and `concurrently` then killed the rest of the session. - **Mitigation:** Keep docs startup behind `scripts/start-docs.mjs`, which now derives the same branch-scoped Portless hostname as the about app and injects that shared public URL into the `/docs` dev proxy target. - **Status:** confirmed ### Worktree shells can miss the repo's pinned Node version - **Date:** 2026-04-03 - **Observed by:** Codex - **Context:** Running `yarn start` in Git worktrees such as `.claude/worktrees/*` or sibling worktree checkouts - **What was surprising:** Some worktree shells resolved `node` and `yarn node` to Homebrew Node `25.2.1` even though the repo pins `22.12.0` in `.nvmrc`, so `yarn start` could silently run the dev launchers under the wrong runtime. - **Impact:** Dev-server behavior can drift between the main checkout and worktrees, making bugs hard to reproduce and violating the repo's expected Node 22 toolchain. - **Mitigation:** Keep the dev launchers behind `scripts/start-dev.mjs` and `scripts/start-docs.mjs`, which now re-exec under the `.nvmrc` Node binary when the current shell is on the wrong version. Shell setup should still prefer `nvm use`. - **Status:** confirmed ### `docs-site/` leftovers can hide missing docs source after the refactor - **Date:** 2026-04-01 - **Observed by:** Codex - **Context:** Post-merge monorepo cleanup after moving the Docusaurus project from `docs-site/` to `docs/` - **What was surprising:** The old `docs-site/` folder can remain on disk with stale but important files like `i18n/`, even after the tracked repo moved to `docs/`. That makes the refactor look duplicated locally and can hide the fact that tracked docs translations were not actually moved into `docs/`. - **Impact:** Agents can delete the old folder as “junk” and accidentally lose the only local copy of docs translations, or keep editing scripts that still point at the dead `docs-site/` path. - **Mitigation:** Treat `docs/` as the only canonical docs project. Before deleting any local `docs-site/` leftovers, restore tracked source like `docs/i18n/` and update scripts and hooks to stop referencing `docs-site`. - **Status:** confirmed ### Multilocale docs preview can spike RAM during verification - **Date:** 2026-04-01 - **Observed by:** Codex - **Context:** Fixing docs i18n, locale routing, and Pagefind behavior with `yarn start:docs` plus Playwright - **What was surprising:** The default docs preview mode now does a full multilocale docs build plus Pagefind indexing before serving, and keeping that process alive alongside multiple Playwright or Chrome sessions can consume much more RAM than a normal Vite or single-locale Docusaurus dev loop. - **Impact:** The machine can become memory-constrained, browser sessions can crash, and interrupted runs can leave stale docs servers or headless browsers behind that keep consuming memory. - **Mitigation:** For docs work that does not need locale-route or Pagefind verification, prefer `DOCS_START_MODE=live yarn start:docs`. Only use the default multilocale preview when you need to validate translated routes or Pagefind. Keep a single Playwright session, close old browser sessions before opening new ones, and stop the docs server after verification if you no longer need it. - **Status:** confirmed ### `translate-docs.py` can leave docs locales half-translated or with broken link targets - **Date:** 2026-04-06 - **Observed by:** Codex - **Context:** Fixing localized docs routes and content after `yarn start:docs` served English detail pages or failed to build locale output - **What was surprising:** The docs translation pipeline had two repo-specific failure modes at once: `scripts/translate-docs.py` only extracted a small subset of `DocsHome` messages when `tr(...)` calls used forms it did not parse, and translated markdown under `docs/i18n/**` could contain machine-translated slugs or `ZXQPLACEHOLDER` artifacts inside link targets. - **Impact:** Localized homepages can silently fall back to English, localized detail pages can appear untranslated, and full `yarn docs:build` can fail on broken locale links even though the source docs are valid. - **Mitigation:** After changing docs translations or regenerating locale files, always run `yarn docs:build` from the repo root, scan `docs/i18n/**` markdown for `ZXQPLACEHOLDER`, and verify translated links still point to canonical doc slugs such as `/apps/5chan/` instead of translated URL paths. If `DocsHome` copy changed, confirm `scripts/translate-docs.py` still extracts all `docs.home.*` messages. - **Status:** confirmed ### About-site no-JS checks must use the Portless route, not a standalone SSR preview - **Date:** 2026-04-12 - **Observed by:** Codex - **Context:** Verifying no-JS support for the `about/` site from a branch worktree - **What was surprising:** A standalone SSR preview can look healthy while the actual branch-scoped Portless route is still serving the wrong app shell or an older process. In this repo, the real local contract is the Portless hostname from `yarn start`, not an ad hoc preview server. - **Impact:** Agents can incorrectly claim no-JS support works, or miss regressions that only show up on `*.bitsocial.localhost`. - **Mitigation:** For `about/` browser verification, always start the real local server with `yarn start` or `yarn start:about` and test the branch-scoped Portless URL first. If a Portless hostname looks stale, inspect and stop the old process before retesting. - **Status:** confirmed ### `chain/` was invisible to `yarn build:verify` and `yarn doctor` - **Date:** 2026-07-05 - **Observed by:** Codex - **Context:** Verifying a chain/-only diff after the `chain/` workspace (standalone Vite app for `chain.bitsocial.net`) was added to the monorepo. - **What was surprising:** `scripts/verify-build.mjs` only recognized `about/`, `docs/`, and `stats/` path prefixes, so a chain/-only diff printed "No targeted build checks matched the current diff" and ran no build at all, even though `build:chain` already existed in the root `package.json`. Separately, `yarn doctor` was hard-coded to `react-doctor about -y`, so React changes under `chain/src` got zero React Doctor coverage. - **Impact:** Agents verifying chain changes had to know to call `yarn build:chain` directly instead of trusting `yarn build:verify`, and React issues in `chain/src` (effects, hooks, dead code) went undetected by `yarn doctor`. - **Mitigation:** `scripts/verify-build.mjs` now has a `chain/` branch mirroring the `about/` one, and `doctor` / `doctor:verbose` now run `react-doctor --project about,chain -y` in a single invocation. `doctor:score` stays `about`-only because `--score` silently prints nothing when combined with `--project` for more than one project; use `yarn react-doctor --project about,chain --verbose -y` (or `--json`) if a chain score is needed. - **Status:** confirmed ### Browser P2P runs on secure WebSockets; pkc-js denies WebRTC and WebTransport by default - **Date:** 2026-08-02 - **Observed by:** Claude - **Context:** Writing landing-page and docs copy about how Bitsocial browser P2P works - **What was surprising:** `@pkcprotocol/pkc-js` ships a default connection gater that rejects WebRTC and WebTransport dials in the browser — `dist/browser/helia/dial-transport-filter.js` exports `DENIED_DIAL_TRANSPORTS_BY_DEFAULT = ["webrtc", "webrtc-direct", "webtransport"]`. Its source comment gives the reason: in the browser those transports add long, often-failing connection-establishment paths (STUN/ICE, certhash rotation) that slow loads, while WebSocket is direct and reliable. Every live peer on the blog's P2P status panel shows "Secure WebSocket". The gater lives in `node_modules`, so nothing in the repo hints at it. - **Impact:** It is very easy to write technically plausible but false public copy — for example crediting WebTransport reaching browser Baseline in March 2026 for making Bitsocial browser P2P possible. That claim shipped into the landing page, the comparison table, and two docs pages before the developer caught it. Wrong architecture claims on public pages are checked by exactly the developer audience the site targets. - **Mitigation:** Never infer which transports Bitsocial uses from what libp2p or the browser platform supports in principle. Check `node_modules/@pkcprotocol/pkc-js/dist/browser/helia/dial-transport-filter.js` for the current deny list, confirm no `connectionGater` override exists under `about/src/`, and read the live transport labels in the blog's "P2P status" panel before making any public claim. The upstream change that actually unblocked browser publishing was the gossipsub monotonic-seqno fix in `@libp2p/gossipsub` 15.0.21 (May 2026); pkc-js currently ships 16.0.4. - **Status:** confirmed ### Relative `./page.md` links from an untranslated docs page break every localized build - **Date:** 2026-08-02 - **Observed by:** Claude - **Context:** Adding a new English-only page, `docs/browser-p2p.md`, that linked to existing docs with `./peer-to-peer-protocol.md` and `./apps/5chan.md` - **What was surprising:** Each locale under `docs/i18n//docusaurus-plugin-content-docs/current/` mirrors the docs tree. A new page absent from those mirrors still renders in every locale via English fallback, but its relative markdown links no longer resolve — Docusaurus emits `/ar/browser-p2p/peer-to-peer-protocol.md/` and fails the build with "Docusaurus found broken links!". Crucially, `yarn build:verify` and `yarn docs:build:verify` build only `en` and pass cleanly; only a full `yarn docs:build` surfaces it, and it aborts on the first locale alphabetically (`ar`). - **Impact:** A docs change can pass every fast local check and still break the production multilocale build. The failure also looks unrelated to the change, since the error names a locale path the author never touched. - **Mitigation:** In any docs page that is not mirrored into `docs/i18n/**`, use root-relative links (`/peer-to-peer-protocol/`, `/apps/5chan/`) instead of relative `.md` links; Docusaurus prefixes them with the locale automatically. `docs/build-your-own-client.md` is the existing example. Run a full `yarn docs:build` — not just `build:verify` — before handing off any change that adds or links a docs page. - **Status:** confirmed ### Concurrent `update-translations.js` runs silently lose keys - **Date:** 2026-08-02 - **Observed by:** Claude - **Context:** Applying 26 translated i18next keys across all 36 locales via the `translate` skill - **What was surprising:** Each invocation is a read-modify-write over all 36 locale files, so two invocations running at once clobber each other and one key vanishes with no error. The `translate` skill explicitly instructs spawning up to 4 subagents concurrently, each of which would call the script. - **Impact:** Fails silently: keys go missing from arbitrary locales, and the diff still looks plausible. - **Mitigation:** Never let translator subagents write locale files concurrently — have them emit dictionary JSON files only, then apply every key serially from the parent agent. After applying, verify programmatically that each key exists in all 35 non-English locales and that no value is byte-identical to the English source. - **Status:** confirmed - **Update (2026-08-10):** The script used to also resolve its target as `path.join(process.cwd(), "public", "translations")`, so the documented repo-root command failed with "Translations directory not found" and had to be run from `about/`. It now resolves the workspace from the current directory or from its own location, and works from anywhere. The concurrency trap above is unchanged. ### The dev react-scan toolbar swallows driven clicks in the bottom-right corner - **Date:** 2026-08-10 - **Observed by:** Claude - **Context:** Verifying the fixed scroll button on the about home page with `playwright-cli` - **What was surprising:** `playwright-cli click` on the fixed scroll button times out with `
intercepts pointer events`. The host element is a zero-height `
` at the end of ``, so it looks harmless in a DOM dump; the toolbar actually lives in its open shadow root and is fixed to the bottom-right corner — the same corner as the app's scroll button. `element.click()` from `eval` bypasses hit-testing and still works, so a scripted check can pass while every real pointer click fails. - **Impact:** A whole verification pass reads as a product bug in the control being tested. `navigator.webdriver` is `false` in playwright-cli sessions, so the app cannot detect automation on its own. - **Mitigation:** `scripts/pw-session.sh open` now registers `window.__NO_DEV_TOOLBAR__ = true` via `page.addInitScript` and reloads, so sessions opened through the wrapper never mount the toolbar. Only the toolbar is suppressed — `react-scan` itself stays enabled, so `__getReactScanReport` and `__ELEMENT_SOURCE__` keep working for the profiler and element-inspection skills. When driving `playwright-cli` directly, set the same flag before load, or expect corner clicks to be intercepted. - **Status:** confirmed ### `skills add` installs Codex and Cursor copies into the gitignored `.agents/` directory - **Date:** 2026-08-18 - **Observed by:** Tommaso + Claude - **Context:** Installing the `improve-threejs` skill from `millionco/react-doctor` with the `skills` CLI (`vercel-labs/skills`). - **What was surprising:** `npx skills add --skill --agent codex` and `--agent cursor` both write to `.agents/skills//`, not to `.codex/skills/` or `.cursor/skills/`. `AGENTS.md` forbids a repo-level `.agents/` directory and `.gitignore:29` ignores it, so both copies are silently untracked. Only `--agent claude-code` writes to the expected `.claude/skills/`. Separately, the documented comma-separated form (`--agent claude-code,codex,cursor`) fails with "Invalid agents" and installs nothing, even though each name is valid on its own. - **Impact:** The install reports success while two of the three toolchain copies land somewhere that will never be committed, so Codex and Cursor silently lack the skill after a fresh clone. The comma form can also produce a no-op install that reads as a success. - **Mitigation:** Run `skills add` once per agent with a single `--agent` value. Use `--agent claude-code` for the `.claude/skills/` copy, then `cp -R .claude/skills/ .codex/skills/` and `cp -R .claude/skills/ .cursor/skills/`, and `rm -rf .agents`. Confirm with `git status` that all three copies show as untracked additions before committing. - **Status:** confirmed ### Long-Running Agent Workflow Source: https://docs.bitsocial.net/agent-playbooks/long-running-agent-workflow/ Path: docs/agent-playbooks/long-running-agent-workflow.md Description: Use this playbook when a task is likely to span multiple sessions, handoffs, or spawned agents. # Long-Running Agent Workflow Use this playbook when a task is likely to span multiple sessions, handoffs, or spawned agents. ## Goals - Give each fresh session a fast way to regain context - Keep work incremental instead of one-shotting a large change - Catch a broken local baseline before adding more code - Leave durable artifacts that the next session can trust ## Where to Keep State - Use `docs/agent-runs//` when humans, review bots, or multiple toolchains need the same task state. - Use a tool-local directory such as `.codex/runs//` only when the task state is intentionally local to one workstation or one toolchain. - Do not hide multi-session shared state in a private scratch file if another contributor or agent will need it later. ## Required Files Create these files at the start of the long-running task: - `feature-list.json` - `progress.md` Use the templates in `docs/agent-playbooks/templates/feature-list.template.json` and `docs/agent-playbooks/templates/progress.template.md`. Prefer JSON for the feature list so agents can update a small number of fields without rewriting the whole document. ## Session Start Checklist 1. Run `pwd`. 2. Read `progress.md`. 3. Read `feature-list.json`. 4. Run `git log --oneline -20`. 5. Run `./scripts/agent-init.sh --smoke`. 6. Choose exactly one highest-priority item that is still `pending`, `in_progress`, or `blocked`. If the smoke step fails, fix the broken baseline before implementing a new feature slice. ## Session Rules - Work on one feature or task slice at a time. - Keep the feature list machine-readable and stable. Update status, notes, files, and verification fields instead of rewriting unrelated items. - Only mark an item verified after running the command or user flow listed in that item. - Use spawned agents for bounded slices, not for overall task-state ownership. - When a child agent owns one item, give it the exact item id, acceptance criteria, and files it may touch. ## Session End Checklist 1. Append a short progress entry to `progress.md`. 2. Update the touched item in `feature-list.json`. 3. Record the exact commands run for verification. 4. Capture blockers, follow-ups, and the next best item to resume. ## Recommended Progress Entry Shape Use a short structure like: ```markdown ## 2026-03-17 14:30 - Item: F003 - Summary: Updated the browser-check flow to use the shared init/bootstrap path. - Files: `.cursor/agents/browser-check.md`, `.codex/agents/browser-check.toml` - Verification: `corepack yarn build:verify`, `corepack yarn lint`, `corepack yarn typecheck` - Next: Run the smoke flow and update the task-board status. ``` ### Skills and Tools Source: https://docs.bitsocial.net/agent-playbooks/skills-and-tools/ Path: docs/agent-playbooks/skills-and-tools.md Description: Use this playbook when setting up/adjusting skills and external tooling. # Skills and Tools Use this playbook when setting up/adjusting skills and external tooling. ## Recommended Skills ### Context7 (library docs) For up-to-date docs on libraries. ```bash npx skills add https://github.com/intellectronica/agent-skills --skill context7 ``` ### Playwright CLI Use `playwright-cli` for browser automation (navigation, interaction, screenshots, tests, extraction). When using `playwright-cli` for repo UI verification, do not stop after one engine. Run the relevant flow in all three main browser engines: - `chrome` for Blink - `firefox` for Gecko - `webkit` for Safari/WebKit coverage Use separate named sessions per engine so evidence stays isolated, but run those sessions sequentially. Only one Playwright browser session may be active at a time, machine-wide, because the contended resource is machine RAM and CPU rather than the repository. Open and close sessions through `./scripts/pw-session.sh`; it holds that shared lock so concurrent agents defer and retry browser work instead of saturating the machine. If an engine is intentionally skipped, record why. During iteration, use Chrome/Blink only. Run the full Chrome, Firefox, and WebKit sequence once the change is ready for final verification. Reuse each engine session for desktop and mobile by resizing it, close it in a finally-style cleanup, and only then open the next engine. ```bash ./scripts/pw-session.sh open verify-chrome https://bitsocial.localhost --browser=chrome playwright-cli -s=verify-chrome snapshot ./scripts/pw-session.sh close verify-chrome ``` When the slot is busy, `open` exits 75; block on `./scripts/pw-session.sh open --wait[=SECONDS] ...` (default 300s) instead of retrying by hand. A lock left behind by an interrupted workflow is reclaimed automatically, because `open` drops any slot whose recorded browser is no longer running. Inspect the holder with `./scripts/pw-session.sh status`; `release ` is a last resort for the rare case where `status` cannot verify the browser state. ```bash npm install -g @playwright/cli@latest playwright-cli install --skills ``` Skill install locations: - `.cursor/skills/playwright-cli/` - `.claude/skills/playwright-cli/` ### Vercel React Best Practices For deeper React/Next performance guidance. ```bash npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-best-practices ``` ### Find Skills Discover/install skills from the open ecosystem. ```bash npx skills add https://github.com/vercel-labs/skills --skill find-skills ``` ## MCP Policy Rationale Avoid GitHub MCP and browser MCP servers for this project because they add significant tool-schema/context overhead. - GitHub operations: use `gh` CLI. - Browser operations: use `playwright-cli`. ## Model Availability - `composer-2` is available only in Cursor. Do not configure it under `.claude/` or `.codex/`. - Codex does not document a `latest` model alias. Committed custom-agent TOMLs under `.codex/**/agents/*.toml` omit both `model` and `model_reasoning_effort` so they inherit the current parent session settings. ### Feature List Template Source: https://docs.bitsocial.net/agent-playbooks/templates/feature-list-template/ Path: docs/agent-playbooks/templates/feature-list-template.mdx Description: Machine-readable template used for long-running multi-session task tracking. # Feature List Template This page mirrors the JSON template shipped in `docs/agent-playbooks/templates/feature-list.template.json` so it can be browsed directly inside the docs site as well. ```json { "task": "replace-with-task-slug", "last_updated": "YYYY-MM-DD", "items": [ { "id": "F001", "priority": 1, "status": "pending", "description": "Describe one end-to-end feature or one reviewable task slice.", "verification": ["List the command or user-visible check that proves this item works."], "files": [], "notes": "" } ] } ``` ### Progress Log Source: https://docs.bitsocial.net/agent-playbooks/templates/progress.template/ Path: docs/agent-playbooks/templates/progress.template.md Description: Append one entry per session. # Progress Log Append one entry per session. ## YYYY-MM-DD HH:MM - Item: F001 - Summary: Replace this with the session summary. - Files: `path/to/file` - Verification: `corepack yarn build:verify`, `corepack yarn lint`, `corepack yarn typecheck` - Blockers: none - Next: Replace this with the next best follow-up. ### Translations Workflow Source: https://docs.bitsocial.net/agent-playbooks/translations/ Path: docs/agent-playbooks/translations.md Description: This project uses i18next translation files in public/translations/{lang}/default.json. # Translations Workflow This project uses i18next translation files in `public/translations/{lang}/default.json`. ## Rule Do not manually edit every language file. Use `scripts/update-translations.js`. ## Add or Update a Key 1. Create a temporary dictionary file, e.g. `translations-temp.json`: ```json { "en": "English text", "es": "Spanish text", "fr": "French text", "de": "German text" } ``` 2. Apply the translation map: ```bash node scripts/update-translations.js --key my_new_key --map translations-temp.json --include-en --write ``` 3. Delete the temporary dictionary file. ## Other Useful Commands ```bash # Copy a key from English to all languages (dry run then write) node scripts/update-translations.js --key some_key --from en --dry node scripts/update-translations.js --key some_key --from en --write # Delete a key from all languages node scripts/update-translations.js --key obsolete_key --delete --write # Audit for unused translation keys node scripts/update-translations.js --audit --dry node scripts/update-translations.js --audit --write ``` ### Content Discovery Source: https://docs.bitsocial.net/content-discovery/ Path: docs/content-discovery.md Description: How Bitsocial separates peer discovery from app-level curation. # Content Discovery Bitsocial does not put one global feed, search index, or ranking algorithm in the protocol. It separates content discovery into two layers: 1. **Network lookup** finds the peers currently serving a known community. 2. **App curation** decides which communities, boards, lists, or posts a product shows first. This keeps the protocol small while leaving room for many discovery experiences to compete. ## Network lookup Every community has a stable address derived from its public key. When a client already knows that address, it queries lightweight HTTP routers to find peers that announced themselves as providers for it. The routers only return provider peer addresses. They do not store posts, metadata, user lists, or a human-readable directory of communities. After the client receives peer addresses, it connects to those peers and fetches the latest community metadata plus content pointers, then fetches the actual post data by hash. This answers the protocol question: "Where can I fetch the latest state for this community?" ## App curation The separate product question is: "Which communities should a user see first?" Bitsocial leaves that to apps, lists, and users instead of baking one answer into the network. Examples include: - a client showing communities the user already follows - a curated default list for a Reddit-style app - directory slots for an imageboard-style app - search or ranking indexes maintained by a specific app - direct links shared by users Apps can index, rank, filter, or highlight different things without turning those choices into protocol law. If one app's discovery surface is not useful, another app can build a different one on the same underlying communities. ## Current apps 5chan currently uses familiar directory paths such as `/b/` or `/g/`. Directory assignments are managed through a public list today, with future versions expected to support in-app board creation and voting for directory slots. Seedit uses default community lists for its front page. Communities can still be created and shared outside that default list. In both cases, the app-level list helps users find something to open, and the protocol-level lookup then resolves the chosen community to peers. ## Why this split matters A single decentralized network still needs good discovery, but the discovery layer should be replaceable. Bitsocial's core protocol focuses on addressability, peer lookup, publishing, and anti-spam. Curation lives above that layer, where apps can experiment with directories, default lists, feeds, search, voting, and moderation policies without requiring a network-wide migration. ### i18n Default Language Policy Source: https://docs.bitsocial.net/i18n-default-language-policy/ Path: docs/i18n-default-language-policy.md Description: Anonymous visitors resolve language in this order: # i18n Default Language Policy Anonymous visitors resolve language in this order: 1. `?lang=` query param 2. Saved language selector choice (`localStorage` in the browser, mirrored to the `i18nextLng` cookie for SSR) 3. Browser/device language when it is one of our supported locales 4. Fall back to `en` Country or region is not used to override a supported browser/device language. For example, `de-DE` resolves to German, `nl-NL` resolves to Dutch, and `pt-PT` resolves to Portuguese because those locale families are supported. If no browser/device language matches a supported locale, visitors receive the English default. ### A complete layman explanation of the Bitsocial protocol Source: https://docs.bitsocial.net/layman-protocol-explanation/ Path: docs/layman-protocol-explanation.md Description: A plain-English walkthrough of Bitsocial communities, peer lookup, publishing, anti-spam challenges, moderation, and apps. # A complete layman explanation of the Bitsocial protocol This page explains Bitsocial without assuming you already understand peer-to-peer networking, cryptographic keys, IPFS, or pubsub. Some details are simplified on purpose. For the more technical version, read the [Peer-to-Peer Protocol](https://docs.bitsocial.net/peer-to-peer-protocol/) page. ## The short version Bitsocial is a protocol for social apps where communities are owned by keys instead of by a company database. A Bitsocial community has an address. Apps use that address to find peers that are serving the community, fetch the latest posts from those peers, and publish new posts through a peer-to-peer message channel. Before a post is accepted, the community can require an anti-spam challenge such as a captcha, invite code, payment, token check, AI moderation check, allowlist, or any other rule that can be coded. That is the core idea: 1. A community is controlled by a private key. 2. The public key gives the community a stable address. 3. Peers help readers find and fetch the community. 4. A community node accepts or rejects new posts. 5. Anti-spam policy belongs to each community, not to one global platform. ## Why hashes matter A hash is a short fingerprint for data. If two people hash the exact same file, they get the same fingerprint. If the file changes, the fingerprint changes. That makes hashes useful for finding and checking data without trusting a company to tell you what the file is. Peer-to-peer systems use this idea constantly. Instead of asking one website for "the file named photo.png", a peer can ask the network for the data with a specific fingerprint. If another peer returns the wrong data, the hash check fails. Bitsocial uses hashes and content identifiers for post data and other pieces of community state. The important point is simple: data can be addressed by what it is, not only by where a company hosted it. ## Why public keys matter A public key and a private key are a matched pair. The private key is secret. It is the thing that gives control. The public key is safe to share. It lets everyone else check that a message, update, or moderation action really came from the matching private key. This is how Bitsocial avoids normal platform accounts. A company does not need to issue the identity. A database row does not need to define the owner. The keypair is the authority. In plain terms: - the private key is the owner's control handle - the public key is the public identity or address - signatures prove that an action came from the owner ## What a Bitsocial community is A Bitsocial community is not just a page in one app. It has its own keypair. The public key gives the community a stable network address. The private key controls updates to the community's state, such as metadata, rules, moderator list, challenge configuration, and the pointers to the latest accepted content. That means a community can outlive one interface. One app can show it as a board. Another app can show it as a forum. A future app can show it in a profile-based feed. The app can change, but the community address still points to the same owned community. ## How reading works When a user opens a Bitsocial community, the app does not ask one central database for the page. The flow is closer to this: 1. The app already knows the community address, or gets it from a list, link, search surface, or human-readable name. 2. The app asks lightweight routers which peers currently provide that community address. 3. The routers return peer addresses only. They do not return posts, rules, profiles, or community metadata. 4. The app connects to peers and fetches the latest community state. 5. That state contains pointers to post content. 6. The app fetches the post content from peers and renders it in a normal social interface. The router is only a lookup helper. It is closer to asking "who has this?" than asking "please serve me the whole website." For more detail on this split, read [Content Discovery](https://docs.bitsocial.net/content-discovery/). ## How posting works Posting is different from reading because open peer-to-peer networks can be spammed. Bitsocial handles publishing through a challenge-response flow: 1. The user writes a post or reply. 2. The app joins the community's peer-to-peer message topic. 3. The app asks the community node for a challenge. 4. The community node sends back the challenge. 5. The user or app completes the challenge. 6. The app sends the post plus the challenge answer. 7. The community node checks the answer and the post. 8. If it passes, the community node accepts the post into the community's next update. 9. Other readers fetch the updated community state from peers. The challenge happens before the post becomes part of the accepted community state. That is the important difference from systems where spam is accepted first and hidden later. ## Why anti-spam challenges matter Most social platforms turn anti-spam into platform policy. One company decides what counts as a valid account, valid post, valid reach, or valid user. Bitsocial separates those things. The protocol gives communities a way to require a challenge before accepting a post, but it does not force every community to use the same challenge. One community might use a captcha. Another might use invite codes. Another might require an SMS check, a payment, an NFT, a token balance, an AI moderation score, a proof of reputation, a community-specific allowlist, or a custom rule. That flexibility matters because spam changes. A protocol-level spam rule becomes stale. A community-level challenge can evolve without migrating the whole network. For the focused explanation, read [Custom Anti-Spam Challenges](https://docs.bitsocial.net/custom-challenges/). ## How moderation works Bitsocial is not moderation-free. It is moderation without one global super-admin. A community can have owners and moderators. Moderator addresses are part of the community state. When a moderator takes an action, that action can be signed. The community node and clients can check the signature against the moderator list. That gives moderation a local scope: - a community owner controls that community - moderators act through keys the community recognizes - apps can still choose what they index, rank, hide, or highlight - no protocol-level company account can erase every identity or seize every community In practice, this means a community can remove spam or enforce rules inside its own space without turning its rules into law for the entire network. For the policy view, read [Local Moderation, Not Global Bans](https://docs.bitsocial.net/local-moderation/). ## What apps add The protocol does not decide what the whole product should look like. An app adds the human experience around the protocol: - default community lists - search and discovery - feeds and ranking - layout and posting interface - media handling - moderation tools - mobile, desktop, or browser packaging - business model and defaults That is why Bitsocial can support different app styles. 5chan can feel like an imageboard. Seedit can feel like forum-style discussion. Other clients can build different discovery surfaces, ranking systems, moderation views, or community defaults while still using compatible Bitsocial communities underneath. The protocol keeps ownership and publishing portable. Apps compete on product quality. ## What public RPC adds Running a peer-to-peer community node directly is powerful, but not everyone wants to manage an always-on machine. Public RPC is the service layer that can make Bitsocial more convenient. A public RPC provider can help users manage communities from a phone or lightweight client, while the long-term ownership model should still let users move away, self-host, or choose a competing provider. The distinction matters: - RPC can help with uptime and convenience - RPC should not become permanent custody - the owner relationship should remain tied to keys, not to one provider's database The first service built on this model is Forge RPC by [Bitsocial Forge](https://bitsocialforge.com/#forge-rpc), which builds on the [Delegated IPNS](https://github.com/pkcprotocol/pkc-js/blob/master/docs/protocol/delegated-ipns.md) protocol design. ## What Bitsocial is not Bitsocial is not a blockchain social network. Social media does not need every post to become a transaction in one global ledger. Bitsocial is not federation in the ActivityPub sense. A community does not need to be an account on one server with one domain, one admin, and one server database. Bitsocial is also not one app. It is a shared protocol layer for apps, communities, nodes, routers, RPC providers, discovery services, anti-spam modules, and moderation tools. The point is not that every user needs to understand all of this before posting. The point is that the product can feel normal while the ownership model underneath is different. ## Where to go next - [Peer-to-Peer Protocol](https://docs.bitsocial.net/peer-to-peer-protocol/) explains the technical flow. - [Content Discovery](https://docs.bitsocial.net/content-discovery/) explains network lookup versus app curation. - [Custom Anti-Spam Challenges](https://docs.bitsocial.net/custom-challenges/) explains the challenge system. - [Identity and Community Ownership](https://docs.bitsocial.net/identity-and-ownership/) explains key-controlled ownership. - [Build your own client](https://docs.bitsocial.net/build-your-own-client/) explains how independent apps can build on the same network. ### BSO Token History Source: https://docs.bitsocial.net/token-history/ Path: docs/token-history.md Description: The full generation history of the BSO token, from its 2021 Avalanche origin to today's immutable, adminless Ethereum contract. # BSO Token History BSO is a provenance coin. The protocol behind Bitsocial is open, and the token and chain are optional by design: anyone can fork the code, run their own client, or build their own economy on top of it. What cannot be forked away is provenance. BSO has been the official Bitsocial token since day one, and every migration since then is verifiable on-chain. This page lists every generation of the token, in order, with full contract addresses so anyone can check the record independently. ## Gen 1: the origin, Avalanche, 2021 - **Chain**: Avalanche - **Year**: 2021 - **Address**: `0x625fc9bb971bb305a2ad63252665dcfe9098bee9` - **Explorer**: [Snowscan](https://snowscan.xyz/address/0x625fc9bb971bb305a2ad63252665dcfe9098bee9) This is where BSO started. The full supply was airdropped, with no presale and no team allocation carved out ahead of the community. The contract was an upgradeable proxy, which was standard practice at the time and let the team ship fixes during the token's early life. ## Gen 2: the move to Ethereum, 2024 - **Chain**: Ethereum - **Year**: 2024 - **Address**: `0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f` - **Explorer**: [Etherscan](https://etherscan.io/token/0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f) Gen 2 moved BSO from Avalanche to Ethereum, where the rest of the Bitsocial Chain roadmap is built. Like Gen 1, this contract was still an upgradeable proxy, kept for one more generation while the final, permanent contract was prepared. ## Gen 3: fully immutable, 2025 - **Chain**: Ethereum - **Year**: 2025 - **Address**: `0xB50cea4c109dc223A10d44c14f521CaeD91DaB5A` - **Explorer**: [Etherscan](https://etherscan.io/token/0xB50cea4c109dc223A10d44c14f521CaeD91DaB5A) Gen 3 is the current and final BSO contract. It is fully immutable and adminless: - no mint function, so supply cannot be inflated - no owner address, so no one can unilaterally change contract behavior - no pause function, so transfers cannot be frozen - no proxy pattern, so the logic itself cannot be swapped out later This is the end state the first two generations were built toward: a token with no admin keys left to hold. ## How the migrations worked Each migration, Gen 1 to Gen 2 and Gen 2 to Gen 3, was a passive 1:1 airdrop. Holders did not need to submit a claim, sign a message, or take any action at all. Balances on the old contract were read directly and mirrored 1:1 onto the new contract, so a holder's position was preserved exactly across the migration. Because both the old and new contracts remain public and on-chain, every step of this process is independently verifiable. Anyone can compare historical holder snapshots from Gen 1 or Gen 2 against current Gen 3 balances and confirm the migration matched what it claimed to do. No part of this history depends on trusting Bitsocial's word for it. ## Verify everything Do not take any of this on faith. Check the record directly: - Gen 1 on [Snowscan](https://snowscan.xyz/address/0x625fc9bb971bb305a2ad63252665dcfe9098bee9) - Gen 2 on [Etherscan](https://etherscan.io/token/0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f) - Gen 3 on [Etherscan](https://etherscan.io/token/0xB50cea4c109dc223A10d44c14f521CaeD91DaB5A) - the current chain site at [chain.bitsocial.net](https://chain.bitsocial.net) If an address does not match what is listed here, it is not the official BSO token. ## Companion project READMEs ### Bitsocial Web Repository: bitsocialnet/bitsocial-web Source: https://github.com/bitsocialnet/bitsocial-web#readme Description: Bitsocial Web is the public web monorepo for Bitsocial. # Bitsocial Web Bitsocial Web is the public web monorepo for Bitsocial. It currently serves: - `https://bitsocial.net/` for the public Bitsocial landing/about site - `https://chain.bitsocial.net/` for the Bitsocial Chain landing site (BSO token and L2 appchain) - `https://docs.bitsocial.net/` for Docusaurus docs - `https://stats.bitsocial.net/` for the Grafana-backed stats dashboard Chain, docs, and stats are served on their own subdomains while remaining in this monorepo. Legacy `/docs` and `/stats` paths on `bitsocial.net` redirect permanently to their dedicated subdomains. ## Repo Layout ```text about/ Public Bitsocial landing/about site chain/ Bitsocial Chain landing site (BSO token and L2 appchain) docs/ Docusaurus docs, i18n files, and contributor playbooks stats/ Grafana, Prometheus, Docker Compose, deploy config, and monitor package scripts/ Shared repo scripts and agent hooks ``` Each top-level subproject has its own local documentation: - [`about/README.md`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/about/README.md) - [`docs/README.md`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/docs/README.md) - [`stats/README.md`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/stats/README.md) The `chain/` workspace is a standalone Vite app for the Bitsocial Chain marketing site. Run it with `corepack yarn start:chain` or build it with `corepack yarn build:chain`. The repo root remains the orchestration layer for installs, verification, and cross-project commands. ## Getting Started Use the pinned Node.js version from [`.nvmrc`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/.nvmrc) and Yarn 4 via Corepack. ```bash nvm install && nvm use corepack enable corepack yarn install ``` The main local URL is: - `https://bitsocial.localhost` Portless keeps a stable named local URL. On non-`master` branches, the repo can fall back to a branch-scoped `*.bitsocial.localhost` route so parallel worktrees do not collide. ## Common Commands ```bash corepack yarn start corepack yarn start:android-usb corepack yarn start:ios-sim corepack yarn llms:generate corepack yarn build:verify corepack yarn build corepack yarn docs:build:verify corepack yarn lint corepack yarn typecheck corepack yarn format:check corepack yarn doctor corepack yarn build:stats-dashboards corepack yarn stats:up corepack yarn stats:down corepack yarn stats:logs ``` ## Verification Before committing code changes, run: ```bash corepack yarn build:verify corepack yarn lint corepack yarn typecheck corepack yarn format:check ``` Use `corepack yarn build` only when you intentionally need the full production build, including the multi-locale docs output. If you changed React UI logic in `about/src/**`, also run: ```bash corepack yarn doctor ``` If dependencies or manifests changed, also run: ```bash corepack yarn deps:check-pinned corepack yarn deps:check-hardened corepack yarn knip ``` If public-facing English content changed, regenerate the LLM indexes: ```bash corepack yarn llms:generate ``` This keeps `about/public/llms*.txt`, `chain/public/llms*.txt`, and `docs/static/llms*.txt` aligned with the landing pages, docs pages, project directory data, public README text, and the generator itself. ## Subproject Notes ### `chain/` - Contains the Bitsocial Chain landing site for BSO and the proposed L2 appchain - Standalone Vite app under `chain/`; served at `https://chain.bitsocial.net/` - Has a translated language-selection shell, while the substantive Chain narrative is currently authored in English ### `about/` - Contains the Bitsocial landing/about site - Currently also contains the public `/projects` catalog and `/apps/:slug` app detail routes - Keeps static assets and translations in `about/public/` - Should not be treated as the long-term home for the project catalog or the blog ### `docs/` - Contains the canonical Docusaurus project - Translation source lives in `docs/i18n/` - Contributor playbooks and long-running agent state also live here ### `stats/` - Contains the Grafana/Prometheus stack and deployment files - The executable monitor service lives in `stats/monitor/` - Public traffic is served at `stats.bitsocial.net`, with Grafana hosted on the VPS origin ### Future Splits - The existing project catalog at `/projects` is expected to be extracted from `about/` into its own subproject, and later may move to `apps.bitsocial.net`. - The blog is also expected to become its own subproject rather than continue to live under `about/`. - `about/` should stay focused on explaining Bitsocial as a whole. ## Translations Landing-site translations live under: - `about/public/translations/{lang}/default.json` The Chain site's translated interface strings live under: - `chain/public/translations/{lang}/default.json` The Chain landing narrative is currently authored in English in `chain/src/sections/`. Docs translations live under: - `docs/i18n/{lang}/...` For translation workflow details, see [`docs/agent-playbooks/translations.md`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/docs/agent-playbooks/translations.md). ## AI Contributor Policy This repo uses tracked AI workflow files and instructions. Read [`AGENTS.md`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/AGENTS.md) before making changes. Relevant local rules also live in: - [`about/AGENTS.md`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/about/AGENTS.md) - [`about/src/AGENTS.md`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/about/src/AGENTS.md) - [`docs/AGENTS.md`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/docs/AGENTS.md) - [`stats/AGENTS.md`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/stats/AGENTS.md) - [`scripts/AGENTS.md`](https://github.com/bitsocialnet/bitsocial-web/blob/HEAD/scripts/AGENTS.md) ## Deployment Shape - `bitsocial.net` is served by Vercel - `chain.bitsocial.net` is served from the standalone Chain app - `docs.bitsocial.net` is served from the docs build - `stats.bitsocial.net` routes to the VPS-hosted Grafana stack - Legacy `/docs` and `/stats` paths on `bitsocial.net` return permanent redirects to those subdomains - `newsletter.bitsocial.net` remains separate ### Verified App Mirror Deployments The `/apps` catalog can show a verified check next to app mirrors when a mirror serves the same HTML entrypoint hash as an official GitHub release artifact. This is meant to attest release parity, not domain ownership. For 5chan and Seedit verified web links, the Vercel `5chan` and `seedit` projects are configured so Git pushes do not automatically create deployments: ```text gitProviderOptions.createDeployments = "disabled" ``` Production domains such as `5chan.cc`, `5channel.org`, `5chan.app`, `seedit.app`, `www.seedit.app`, and `p2p.seedit.app` should only be updated from a GitHub release or tag deployment. Do not point those domains at a raw `master` deployment unless that deployment has first been promoted into an official release and its static HTML artifact hash has been checked. When adding or updating mirror verification metadata in `about/src/lib/apps-data.ts`, verify the live mirror HTML against the matching release ZIP before showing the badge. If the live hash does not match a GitHub release artifact, leave the mirror unverified until the mirror is redeployed from the release or a new release is cut for the served artifact. ## Commit Workflow This repo uses Commitizen for Conventional Commits. - Interactive: `corepack yarn commit` - Non-interactive: `git commit -m "type(scope): message"` The Husky Commitizen hook may print a `/dev/tty` warning in non-interactive shells, but the commit can still succeed when the message is already provided. Use `--no-verify` only when you explicitly need to bypass local hooks. ### 5chan Repository: bitsocialnet/5chan Source: https://github.com/bitsocialnet/5chan#readme Description: 5chan is a serverless, adminless, decentralized and open source imageboard built on the Bitsocial protocol. It features the classic imageboard directory structure, but with a crucial difference: anyone can create and own boards, and multiple boards can compete for each directory slot . # 5chan 5chan is a serverless, adminless, decentralized and open-source imageboard built on the [Bitsocial protocol](https://bitsocial.net). It features the classic imageboard directory structure, but with a crucial difference: **anyone can create and own boards, and multiple boards can compete for each directory slot**. ## Key Features ### Decentralized Board Ownership Unlike traditional imageboards, 5chan has no global admins or central authority. Anyone can create unlimited boards using [5chan Board Manager](https://github.com/bitsocialnet/5chan-board-manager). Each board owner runs their own P2P node that users connect to peer-to-peer, giving them complete control over their board's content, moderation, and rules. ### Competitive Directory System 5chan maintains the familiar imageboard directory structure (Japanese Culture, Video Games, Interests, Creative, etc.), but introduces competition: **multiple boards can compete for each directory slot**. For example, there can be unlimited "Business & Finance" boards, but only the highest-voted one appears in the directory on the homepage. Currently, directory assignments are temporarily handpicked by developers through GitHub pull requests. In the future, each directory will have its own voting page. 5chan Pass holders are expected to participate in directory voting, while final governance mechanics are still being designed to include BSO-holder alignment instead of pass-only final control. ### How It Works - **Current System**: Developers manually curate directory assignments by reviewing pull requests to the [5chan directory files](https://github.com/bitsocialnet/lists/tree/master/5chan-directories). - **Future System**: Each directory will have its own voting page listing the boards competing for that slot. 5chan Pass holders are expected to participate in directory voting, while final governance mechanics are still being designed to include BSO-holder alignment instead of pass-only final control. - **Accessing Boards**: Users can access any board at any time using its address, regardless of directory assignment. Boards can be accessed via the search bar, by subscribing to them (which adds them to the top bar), or by directly navigating to their address. ### Future Roadmap #### In-App Board Creation Creating boards directly from the 5chan web app (5chan.app) is planned. This requires connecting via RPC to a bitsocial node—technically already possible, but there's no default connection configured. A default connection would require a public RPC service (similar to what Infura provides for crypto wallets, but for bitsocial nodes). This would allow all users to be connected to a P2P node by default using a free tier subscription in the background, potentially monetized via ads injected in the RPC service-owned boards. #### Directory Voting Directory voting pages are planned for each slot on 5chan. These pages will list the competing boards for that directory, and 5chan Pass holders are expected to participate in voting. Final governance mechanics are still being designed to include BSO-holder alignment instead of pass-only final control. ## Downloads - **Web version**: https://5chan.app (also available using Brave/IPFS Companion on https://5chan.eth) - **Desktop version** (full P2P bitsocial node, seeds automatically): Available for Mac/Windows/Linux, [download from the release page](https://github.com/bitsocialnet/5chan/releases/latest) - **Mobile version**: Available for Android, [download from the release page](https://github.com/bitsocialnet/5chan/releases/latest) ## Run 5chan in Your Browser With a Local Node If you want the full P2P node but prefer opening 5chan in your normal browser instead of using the desktop app, use [bitsocial-cli](https://github.com/bitsocialnet/bitsocial-cli). It runs the Bitsocial/IPFS node and serves the bundled 5chan Web UI locally, so you do not need to run this repository separately. ```sh-session npm install -g @bitsocial/bitsocial-cli bitsocial daemon ``` When the daemon starts, it prints a `WebUI (5chan - Imageboard-style UI)` URL. Open that URL in your browser to use 5chan through your local node. See the [bitsocial-cli daemon docs](https://github.com/bitsocialnet/bitsocial-cli#running-daemon) for details. ## Creating a Board In the bitsocial protocol, a 5chan board is called a _community_. To deliver the expected 5chan imageboard UX, a board should run on a bitsocial node together with [5chan Board Manager](https://github.com/bitsocialnet/5chan-board-manager). The board manager applies imageboard-style lifecycle rules that bitsocial communities do not enforce by themselves: thread limits, bump limits, archived-thread retention, and purging of author-deleted content. To create and run a board: 1. Follow the Docker Compose flow in the [5chan-board-manager README](https://github.com/bitsocialnet/5chan-board-manager#docker-compose-recommended); 2. Create your community and add it to 5chan Board Manager using the commands shown there; 3. Keep the manager running so it can apply 5chan board behavior such as thread archiving, bump limits, and retention cleanup. Once created, anyone can connect to your community using any bitsocial client (such as 5chan) by using the community address. The address is not stored in any central database—bitsocial is a pure peer-to-peer protocol. Without 5chan Board Manager, a community can still be opened in 5chan, but it will not behave like a conventional imageboard board: old threads will not be archived when they fall past the last page, bump limits will not be enforced, archived threads will not be purged after the retention window, and author-deleted content will not be automatically purged. **Note**: Creating boards directly from the 5chan web app is planned for the future (see [Future Roadmap](#future-roadmap)). ## Submitting Your Board to a Directory To have your board appear in a directory on the 5chan homepage: 1. Ensure your board meets these requirements: - Active and well-moderated - Relevant to the directory category - **99% uptime** (since a board acts like its own server—it's a P2P node) 2. Open a pull request on GitHub by editing the relevant file in the [5chan-directories folder](https://github.com/bitsocialnet/lists/tree/master/5chan-directories) 3. Add your board's entry with: - Title: in the format `/directoryCode/ - Title`, e.g. "/biz/ - Business & Finance"; - Address: the bitsocial community address, whether IPNS key (`12KooW...`) or readable crypto address (`mydomain.eth`); - NSFW status: `true` or `false`, must match the standard classification for the directory code. 4. The developers will review your PR and merge it if approved **Note**: Even if your board isn't assigned to a directory, users can still access it at any time using its bitsocial community address. Directory assignment only affects visibility on the homepage. ## Development ### Prerequisites - Node.js 22.12.0, pinned in [`.nvmrc`](https://github.com/bitsocialnet/5chan/blob/HEAD/.nvmrc) - Corepack enabled once per machine: `corepack enable` ### Contributor Setup 1. Run `nvm install && nvm use` 2. Run `corepack enable` once 3. Use plain `yarn install`, `yarn build`, and `yarn test` ### Setup 1. Clone the repository 2. Install dependencies: `yarn install` 3. Start the web client: `yarn start` The dev server normally runs at https://5chan.localhost via [Portless](https://github.com/vercel-labs/portless), which gives each Bitsocial project a stable, named URL instead of a random port. Portless 0.11 serves this URL through an HTTPS proxy on port 443, so the first `yarn start` after install or proxy reset may prompt for sudo; accept the prompt so the URL can stay portless. On non-`master` branches, or when another legacy process is already holding the canonical route, `yarn start` will automatically use a branch-scoped `*.5chan.localhost` URL instead of failing. To bypass Portless and use a plain Vite dev server, run `PORTLESS=0 yarn start`; it will start at `http://localhost:3000` and automatically fall forward to the next free port if `3000` is already in use. For device testing on a USB-connected Android phone (without relying on `5chan.localhost` DNS from the device): - `yarn start:android-usb` starts Vite bound to `127.0.0.1` and runs `adb reverse`, so the phone can load the dev site at `http://localhost:3000`. When the server is up, it opens that URL in each connected device’s default browser via `adb`. Set `ANDROID_USB_OPEN_BROWSER=0` to skip auto-open. Requires [Android platform-tools](https://developer.android.com/tools/releases/platform-tools) (`adb` on your `PATH`), USB debugging enabled, and the device showing as `device` in `adb devices`. ### Scripts - **Web client**: `yarn start` (https://5chan.localhost) - **Web client (Android phone over USB)**: `yarn start:android-usb` (see above) - **Electron client** (must start web client first): `yarn electron` - **Electron client** (don't delete data): `yarn electron:no-delete-data` - **Web client and electron client**: `yarn electron:start` - **Web client and electron client** (don't delete data): `yarn electron:start:no-delete-data` ### Challenge Types Bitsocial communities can require users to solve one or more anti-spam challenges before a publication is accepted. 5chan already supports multiple challenge types, including `url/iframe` challenges so [Mintpass](https://github.com/bitsocialnet/mintpass) communities can run their iframe flow directly inside a modal. The modal first shows a hostname confirmation (showing only the host for mintpass.org, full URL otherwise), then opens the HTTPS iframe with the current theme, replaces `{userAddress}` tokens with the signed-in address, and submits automatically when the user finishes. 5chan also has an optional UX integration with [`@bitsocial/ai-moderation-challenge`](https://github.com/bitsocialnet/ai-moderation-challenge): when a failed verification contains the exact message `This media was already posted recently.`, 5chan abandons the rejected pending publication, returns to the originating post form, preserves the draft, and shows the message inline. This string is an intentionally small cross-package contract rather than a package dependency. Challenges that omit it, or return any other error, continue through the normal generic challenge-error flow. ### Build The Linux/Windows/macOS/Android build scripts are in [.github/workflows/release.yml](https://github.com/bitsocialnet/5chan/blob/master/.github/workflows/release.yml) ## License 5chan is open-source software (GPL-3.0-or-later) with no owner—anyone can host their own instance on any domain. The operator of any domain is merely hosting the web app and does not own, create, moderate, or control 5chan or any board content, which is stored peer-to-peer and generated by board owners and users. ### Seedit Repository: bitsocialnet/seedit Source: https://github.com/bitsocialnet/seedit#readme Description: Seedit is a serverless, adminless, decentralized and open source (old)reddit alternative built on the Bitsocial protocol. Like reddit, anyone can create a seedit community. Unlike reddit, communities are independently owned, subscriptions point directly to community addresses, and Seedit's default communities can evolve with the network. # Seedit Seedit is a serverless, adminless, decentralized and open-source (old)reddit alternative built on the [Bitsocial protocol](https://bitsocial.net). Like reddit, anyone can create a seedit community. Unlike reddit, communities are independently owned, subscriptions point directly to community addresses, and Seedit's default communities can evolve with the network. - Seedit web version: https://seedit.app — or, using Brave/IPFS Companion: https://seedit.eth ### Downloads - Seedit desktop version (full p2p bitsocial node, seeds automatically): available for Mac/Windows/Linux, [download link in the release page](https://github.com/bitsocialnet/seedit/releases/latest) - Seedit mobile version: available for Android, [download link in the release page](https://github.com/bitsocialnet/seedit/releases/latest) ## How to create a community To run a community, you can choose between two options: 1. If you prefer to use a **GUI**, download the desktop version of the Seedit client, available for Windows, MacOS and Linux: [latest release](https://github.com/bitsocialnet/seedit/releases/latest). Create a community using using the familiar old.reddit-like UI, and modify its settings to your liking. The app runs an IPFS node, meaning you have to keep it running to have your board online. 2. If you prefer to use a **command line interface**, install bitsocial-cli, available for Windows, MacOS and Linux: [latest release](https://github.com/bitsocialnet/bitsocial-cli/releases/latest). Follow the instructions in the readme of the repo. When running the daemon for the first time, it will output WebUI links you can use to manage your community with the ease of the GUI. Peers can connect to your bitsocial community using any bitsocial client, such as Seedit or [5chan](https://github.com/bitsocialnet/5chan). They only need the community address, which is not stored in any central database, as bitsocial is a pure peer-to-peer protocol. ### How to add a default community Seedit's versioned default communities are published in Bitsocial's [seedit-default-subscriptions.json list](https://github.com/bitsocialnet/lists/blob/master/seedit-default-subscriptions.json). New accounts subscribe to these communities by default. When the list changes, existing users can review the update and choose which additions to join; Seedit never removes a manually chosen subscription. You can open a pull request in that repository to propose a community for the list. ### How directory routes work Seedit separates short discovery routes from subscriptions. A route such as `/s/pics` always opens the finalized winner of the `pics` directory, so valuable short routes are not permanently controlled by squatters or global administrators. The topbar therefore displays `pics`, regardless of the winner's exact address. The directory model is designed for decentralized voting; during bootstrap, finalized snapshots are published through [bitsocialnet/lists](https://github.com/bitsocialnet/lists). Joining still subscribes to that winner's exact community address, and the home feed reads those exact subscriptions directly. Directory winner changes never silently replace a subscription unless the user explicitly enabled automatic switching; otherwise Seedit lets the user switch, keep both communities, or keep the current one. Post permalinks also use exact community addresses so shared links remain durable. See the [directory-routes architecture decision](https://github.com/bitsocialnet/seedit/blob/HEAD/docs/architecture/directory-routes.md) for the complete model. ## Contributor setup 1. `nvm install && nvm use` 2. Run `corepack enable` once on your machine 3. Use plain `yarn install`, `yarn build`, and `yarn test` from then on ## To run locally 1. `yarn install` to install Seedit dependencies 2. `yarn start` to run the web client The default web dev server runs at `https://seedit.localhost` via [Portless](https://github.com/vercel-labs/portless), so it can share the same proxy as other Bitsocial projects without colliding on raw Vite ports. On non-`master` branches, or when another legacy process is already holding the canonical route, `yarn start` automatically uses a branch-scoped `*.seedit.localhost` URL instead of failing, and repeated branch-scoped runs keep suffixing (`-2`, `-3`, ...) until they find a free route. To bypass Portless and use plain Vite directly, run `PORTLESS=0 yarn start`; it will probe from port `3000` unless you pin `PORT` yourself. ### Scripts: - Web client: `yarn start` (`https://seedit.localhost`) - Electron client (must start web client first): `yarn electron` - Electron client and don't delete data: `yarn electron:no-delete-data` - Web client and electron client: `yarn electron:start` (forces `PORTLESS=0 PORT=3000` and uses `http://localhost:3000`) - Web client and electron client and don't delete data: `yarn electron:start:no-delete-data` (forces `PORTLESS=0 PORT=3000` and uses `http://localhost:3000`) ### Build: The linux/windows/mac/android build scripts are in https://github.com/bitsocialnet/seedit/blob/master/.github/workflows/release.yml ### bitbones Repository: bitsocialnet/bitbones Source: https://github.com/bitsocialnet/bitbones#readme Description: bitbones is a bare bones GUI client for the Bitsocial protocol — the smallest useful surface over @bitsocial/bitsocial react hooks. It exists to make the protocol legible: almost no styling, no product opinions, and every view a thin wrapper over one hook, so it is the fastest place to reproduce a bug or try a hooks change against real communities. # bitbones bitbones is a bare bones GUI client for the [Bitsocial protocol](https://bitsocial.net) — the smallest useful surface over [`@bitsocial/bitsocial-react-hooks`](https://github.com/bitsocialnet/bitsocial-react-hooks). It exists to make the protocol legible: almost no styling, no product opinions, and every view a thin wrapper over one hook, so it is the fastest place to reproduce a bug or try a hooks change against real communities. - Web version: https://bitbones.app - Desktop version (full p2p bitsocial node, seeds automatically): available for Mac/Windows/Linux, [download from the release page](https://github.com/bitsocialnet/bitbones/releases/latest) - Mobile version: available for Android, [download from the release page](https://github.com/bitsocialnet/bitbones/releases/latest) ## Switching default community lists bitbones does not curate its own default communities. The feed reads whichever client's list you pick, straight from [bitsocialnet/lists](https://github.com/bitsocialnet/lists): - **seedit** — [`seedit-default-subscriptions.json`](https://github.com/bitsocialnet/lists/blob/master/seedit-default-subscriptions.json), the exact communities new Seedit accounts subscribe to. - **5chan** — [`5chan-directories/`](https://github.com/bitsocialnet/lists/tree/master/5chan-directories), resolved the way 5chan resolves it: one winning board per directory code, ranked by score, then by when it was added, then by address. The switch sits at the left of the menu bar on the `p/all` feed and shows both client marks, the active one lit. The selection persists in `localStorage` under `bitbonesDefaultList`. Both lists are also vendored into `src/data/` at build time by `yarn sync:lists`, so bitbones still has a feed when GitHub is unreachable. seedit is the default because its list is 10 communities against 5chan's 64, and every extra community is another name resolution and page fetch on a cold peer-to-peer start. ## Development ```sh-session nvm install nvm use corepack enable yarn install yarn start ``` | command | what it does | | --- | --- | | `yarn start` | vite dev server on http://localhost:5173 | | `yarn build` | production build into `build/` | | `yarn sync:lists` | refresh the vendored default community lists from bitsocialnet/lists | | `yarn lint` | oxlint | | `yarn type-check` | `tsc --noEmit` over `src/` | | `yarn prettier` | oxfmt | | `yarn knip` | unused/undeclared dependency check | | `yarn electron:dev` | run the desktop app against the dev server | | `yarn electron:make` | package the desktop app | ## Origins bitbones is a port of an existing GPL bare-bones client onto the Bitsocial stack: `@bitsocial/bitsocial-react-hooks` for all protocol access, communities throughout, and the dependency stack brought up to the same versions 5chan and Seedit run. The first commit in this repository is the unmodified upstream source, so the entire port is reviewable as a single diff. ## License GPL-3.0-or-later — see [LICENSE](https://github.com/bitsocialnet/bitbones/blob/HEAD/LICENSE). ### HuggingSocial Repository: Rinse12/huggingsocial Source: https://github.com/Rinse12/huggingsocial#readme Description: Website: huggingsocial.co # HuggingSocial **Website: [huggingsocial.co](https://huggingsocial.co/)** **BitTorrent moves the weights. BitSocial keeps the index and the conversation out of anyone's hands.** HuggingSocial is a peer-to-peer place to find, discuss and download open AI models and datasets. It is a model hub that no company owns: the list of models, the model cards, the comments and the votes all live on a peer-to-peer network, and the model files themselves are fetched over BitTorrent (or plain HTTP mirrors), verified on your machine with checksums. ## Why is this needed? Almost all open AI models live on one website, Hugging Face. In August 2026 Nvidia agreed to buy it for roughly $13 billion. Whatever you think of Nvidia, the open-model ecosystem now has a single owner for its hosting, and the places where people *talk* about models (Reddit, Discord) are also companies that can change their rules, sell, or be pressured at any time. When one entity controls where models are stored, the next thing that gets controlled is which models are visible, which are removed, and which conversations are allowed. Hugging Face has already removed models it did not like ([GPT-4chan](https://thegradient.pub/gpt-4chan-lessons/)), and corporate takedowns have wiped community copies ([Meta's DMCA against 403 LLaMA repositories](https://github.com/github/dmca/blob/master/2023/03/2023-03-21-meta.md)). A torrent index would help with storage, but torrent indexes get taken down too, and a torrent tells you nothing about *who* published a file. HuggingSocial fixes the three parts together: - **Discovery.** The index of models is held by communities ("hubs") that are controlled by cryptographic keys, not by accounts on a website. Nobody can delete a hub except its owner. - **Provenance.** Every model listing is signed by whoever published it, and every download is checked against the checksums in that listing. A poisoned re-upload will simply fail verification. - **Discourse.** Model cards, benchmarks, "does this quant run on my GPU", fine-tune requests and license debates happen in the same hubs, with moderation set by each hub, not by a platform. ## How it works (short version) - **Hubs** are communities on [BitSocial](https://bitsocial.net), a peer-to-peer social protocol built on IPFS/libp2p. Each hub sets its own rules and anti-spam challenge. - A **model listing** is a post in a hub: name, version, license, model card, file checksums, and a list of download sources (Hugging Face URL, magnet link, IPFS, …). - The **client** (web app, or desktop app with a built-in torrent engine) downloads from any of those sources, verifies the files, and can keep seeding them so others get them faster. - **HuggingSocial hosts nothing.** We build the client. Anyone can run a hub, an index, or a seedbox. ## Status In design and early development. Sign up to the newsletter on the website to hear when there's something to run. Technical design is in [DESIGN.md](https://github.com/Rinse12/huggingsocial/blob/HEAD/DESIGN.md); open work is in [TODO.md](https://github.com/Rinse12/huggingsocial/blob/HEAD/TODO.md). ## License Free software, [GPL-3.0-or-later](https://github.com/Rinse12/huggingsocial/blob/HEAD/LICENSE): the clients, the tooling, the website. Contributions welcome: pick something from [TODO.md](https://github.com/Rinse12/huggingsocial/blob/HEAD/TODO.md) or open an issue. ### MintPass - NFT Authentication Middleware for Bitsocial Repository: bitsocialnet/mintpass Source: https://github.com/bitsocialnet/mintpass#readme Description: MintPass is an NFT based authentication system that provides verified identity proofs for decentralized communities. It began as an anti‑spam challenge for Bitsocial communities, and it works equally well for other protocols and social applications. Users mint a non‑transferable verification NFT (e.g., after SMS OTP) that communities can check to reduce sybil attacks, such as fake upvotes/downvotes, fake conversations, and users evading bans. # MintPass - NFT Authentication Middleware for Bitsocial MintPass is an NFT-based authentication system that provides verified identity proofs for decentralized communities. It began as an anti‑spam challenge for Bitsocial communities, and it works equally well for other protocols and social applications. Users mint a non‑transferable verification NFT (e.g., after SMS OTP) that communities can check to reduce sybil attacks, such as fake upvotes/downvotes, fake conversations, and users evading bans. ## How people use MintPass 1) Visit `mintpass.org/request`, enter a phone number, and complete SMS OTP. 2) MintPass mints an NFT (on testnet in this reference deployment) to your wallet or records an equivalent “verified” state when on‑chain minting is disabled. 3) Communities (e.g., Bitsocial communities) check ownership of the NFT to treat you as authenticated for anti‑spam. The request form looks like this: ## Project Structure ``` mintpass/ ├── contracts/ # MintPassV1 smart contract and tooling ├── challenge/ # Bitsocial challenge implementation (“mintpass”) ├── web/ # Next.js website + API (mintpass.org) ├── docs/ # Documentation and specifications ├── tests/ # Cross‑component integration tests └── scripts/ # Deployment and utilities ``` ### Subprojects - `contracts/`: Solidity contracts (MintPassV1). Versioned, role‑based minting, token types per NFT (type 0 = SMS). See `contracts/README.md`. - `challenge/`: The Bitsocial challenge that checks for a MintPass NFT and applies additional rules (e.g., transfer cooldowns) to resist sybils. - `web/`: The user‑facing site and serverless backend. Sends SMS codes, verifies OTP, and mints or records successful verification. See `web/README.md`. ## Privacy and anti‑sybil design (high level) - Short‑lived operational data (OTP codes, verification markers, rate‑limit state) stored in Redis with TTLs. - Persistent “mint association” between wallet and phone to prevent duplicate mints. - Optional IP reputation (VPN/proxy) and phone‑risk checks, optional geoblocking, and per‑IP cooldowns. - Secrets live only in environment variables; logs avoid PII and never include OTPs or private keys. ## Getting started 1. Run `nvm install && nvm use` 2. Run `corepack enable` once per machine so `yarn` resolves to the pinned Yarn 4 release 3. Use plain `yarn install`, `yarn build`, and `yarn test` - Contracts: `cd contracts && yarn install && yarn test` - Challenge: `cd challenge && yarn install && yarn test` - Web: `cd web && yarn install && yarn dev` then open `https://mintpass.localhost/request` ## Using MintPass in your community Community owners add the MintPass challenge to their community settings. When enabled, every publication (post, reply, vote) requires the author to hold a valid MintPass NFT. The challenge is published as [`@bitsocial/mintpass-challenge`](https://www.npmjs.com/package/@bitsocial/mintpass-challenge) on npm. ### With pkc-js over RPC If your RPC server is already running, first install the challenge on the server: ```bash bitsocial challenge install @bitsocial/mintpass-challenge ``` Then from your RPC client, connect and set the challenge on your community by name — no npm install or challenge registration needed on the client side: ```ts import PKC from "@pkcprotocol/pkc-js"; const pkc = await PKC({ pkcRpcClientsOptions: ["ws://localhost:9138"] }); const community = await pkc.createCommunity({ address: "your-community-address.bso" }); await community.edit({ settings: { challenges: [ { name: "@bitsocial/mintpass-challenge", options: { chainTicker: "base", contractAddress: "0x13d41d6B8EA5C86096bb7a94C3557FCF184491b9", requiredTokenType: "0", transferCooldownSeconds: "604800" } } ] } }); ``` ### With pkc-js (TypeScript) Install the challenge package: ```bash npm install @bitsocial/mintpass-challenge ``` Register the challenge and configure your community: ```typescript import PKC from '@pkcprotocol/pkc-js' import mintpassChallenge from '@bitsocial/mintpass-challenge' // Register the challenge so it can be referenced by name PKC.challenges['@bitsocial/mintpass-challenge'] = mintpassChallenge const pkc = await PKC({ /* your pkc options */ }) const community = await pkc.createCommunity({ address: 'your-community.bso' }) await community.edit({ settings: { challenges: [{ name: '@bitsocial/mintpass-challenge', options: { chainTicker: 'base', contractAddress: '0x13d41d6B8EA5C86096bb7a94C3557FCF184491b9', requiredTokenType: '0', bindToFirstAuthor: 'true', transferCooldownSeconds: '604800', } }] } }) ``` #### Challenge options All option values must be strings (pkc-js challenge convention). | Option | Default | Description | |--------|---------|-------------| | `chainTicker` | `"base"` | Chain where MintPass is deployed | | `contractAddress` | Base Sepolia default | Contract address (auto-detected for supported chains) | | `requiredTokenType` | `"0"` | Token type (0 = SMS, 1 = Email) | | `bindToFirstAuthor` | `"true"` | Bind NFT to first author per community | | `transferCooldownSeconds` | `"604800"` | Cooldown after NFT transfer (1 week) | | `error` | Default message | Custom error (`{authorAddress}` placeholder supported) | | `rpcUrl` | Chain default | Optional custom RPC URL | ### With bitsocial-cli Install the challenge package: ```bash bitsocial challenge install @bitsocial/mintpass-challenge ``` Edit your community to use the challenge: ```bash bitsocial community edit your-community.bso \ '--settings.challenges[0].name' @bitsocial/mintpass-challenge \ '--settings.challenges[0].options.chainTicker' base \ '--settings.challenges[0].options.contractAddress' '0x13d41d6B8EA5C86096bb7a94C3557FCF184491b9' \ '--settings.challenges[0].options.requiredTokenType' '0' \ '--settings.challenges[0].options.bindToFirstAuthor' 'true' \ '--settings.challenges[0].options.transferCooldownSeconds' '604800' ``` See the [bitsocial-cli documentation](https://github.com/bitsocialnet/bitsocial-cli) for full CLI reference. ## Where MintPass is useful While designed for Bitsocial, any decentralized or serverless social app can use MintPass NFTs as a lightweight proof‑of‑personhood. Apps only need to check ownership of a token type (e.g., type 0 for SMS) to gate actions or increase trust in votes and reports. ## Roadmap and considerations We plan to support multiple authentication methods alongside SMS OTP to fit different threat models and UX constraints: - Add a “pay‑to‑mint” option with a small fee that is high enough to deter bulk purchases but low enough for regular users. - Add additional human‑verification signals (e.g., email, government‑backed KYC providers, or proofs such as biometrics/world‑ID systems) when they can be integrated without compromising decentralization goals. - Expand admin tooling, heuristics, and optional device signals to further reduce abuse. These items are exploratory; concrete work will land incrementally and stay configurable so communities can choose what they trust. ## Technology Stack - **Smart Contracts**: Solidity, Hardhat/Foundry - **Website**: Next.js, React, Ethereum (ethers) - **Challenges**: TypeScript, pkc-js integration - **Deployment**: Base network (L2) ## License MintPass is fully open source — you're free to run your own. It's a mixed-license monorepo (see [LICENSE](https://github.com/bitsocialnet/mintpass/blob/HEAD/LICENSE)): - `web/` (the mintpass.org hosted site + API): AGPL-3.0-or-later - `challenge/` (`@bitsocial/mintpass-challenge`): GPL-3.0-or-later - `contracts/` (`@bitsocial/mintpass-contracts`): MIT A hosted version is available at [mintpass.org](https://mintpass.org). ### @bitsocial/ai-moderation-challenge Repository: bitsocialnet/ai-moderation-challenge Source: https://github.com/bitsocialnet/ai-moderation-challenge#readme Description: Automatic PKC challenge that evaluates Bitsocial comment content against community.rules with an OpenAI compatible model endpoint. The package runs on the community node and does not require a hosted Bitsocial moderation server. # @bitsocial/ai-moderation-challenge Automatic PKC challenge that evaluates Bitsocial comment content against `community.rules` with an OpenAI-compatible model endpoint. The package runs on the community node and does not require a hosted Bitsocial moderation server. ## Installation ```bash bitsocial challenge install @bitsocial/ai-moderation-challenge ``` ## Configuration Install this challenge twice: one `allow` branch and one `review` branch. The `review` branch uses PKC `pendingApproval` to route rule-breaking comments to the moderator queue, and returns the AI model reason in `commentUpdate.reason` when the installed PKC runtime supports pending-approval metadata. ```js [ { name: "@bitsocial/spam-blocker-challenge" }, { name: "@bitsocial/ai-moderation-challenge", options: { apiUrl: "https://api.x.ai/v1/chat/completions", apiFormat: "chat-completions", apiKey: "xai-...", model: "grok-4.6", reasoningEffort: "high", triageApiKey: "sk-...", triageModel: "gpt-5.6-luna", triageReasoningEffort: "none", branch: "allow", promptUrl: "https://prompt.example.com/v1/prompts/ai-moderation.md", promptBearerToken: "shared-secret-token" }, exclude: [{ challenges: [2] }] }, { name: "@bitsocial/ai-moderation-challenge", options: { apiUrl: "https://api.x.ai/v1/chat/completions", apiFormat: "chat-completions", apiKey: "xai-...", model: "grok-4.6", reasoningEffort: "high", triageApiKey: "sk-...", triageModel: "gpt-5.6-luna", triageReasoningEffort: "none", branch: "review", promptUrl: "https://prompt.example.com/v1/prompts/ai-moderation.md", promptBearerToken: "shared-secret-token" }, pendingApproval: true, exclude: [{ challenges: [1] }] } ]; ``` Challenge options are private community-node settings in `pkc-js`: nothing in `options` is copied into the public community challenge metadata unless the owner names it in `publicOptions` (see [Settings validation and public options](#settings-validation-and-public-options)). Keep local settings backups private because they can contain provider keys or prompt access tokens. Production operators should keep the real moderation prompt in a private node-local file referenced by `promptPath`, or in a private HTTPS endpoint referenced by `promptUrl` plus `promptBearerToken`. Do not commit production prompts to public repositories; the built-in prompt is only a public fallback and the challenge emits a warning when it is used. ## Options | Option | Default | Description | | ----------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `apiUrl` | `https://api.openai.com/v1/responses` | Full OpenAI-compatible endpoint URL | | `apiFormat` | `responses` | Request/response format: `responses` or `chat-completions` | | `apiKey` | none | Private provider API key; leave empty for self-hosted endpoints that do not require one | | `model` | `gpt-5.4-nano` | Model name sent to the provider | | `fallbackModel` | none | Secondary model used once when the primary model returns HTTP 429 | | `reasoningEffort` | none | Optional primary-model reasoning effort: `none`, `low`, `medium`, `high`, `xhigh`, or `max` | | `triageApiUrl` | `https://api.openai.com/v1/responses` | Full endpoint URL for the optional first-pass triage model | | `triageApiFormat` | `responses` | Triage request/response format: `responses` or `chat-completions` | | `triageApiKey` | none | Private triage-provider API key | | `triageModel` | none | Optional first-pass model; setting it enables the two-stage cascade | | `triageReasoningEffort` | none | Optional triage-model reasoning effort: `none`, `low`, `medium`, `high`, `xhigh`, or `max` | | `branch` | `allow` | Branch mode: `allow` or `review` | | `prompt` | built-in prompt | Private inline system prompt text | | `promptPath` | none | Private file path for a system prompt on the community node; `~` expands to the home directory | | `promptUrl` | none | Private HTTPS URL for a remotely hosted system prompt | | `promptBearerToken` | none | Private bearer token sent only when fetching `promptUrl` | | `cachePath` | `~/.bitsocial-ai-moderation-cache.json` | Private JSON verdict cache path; set to an empty string to disable persistent caching | | `auditLogPath` | `~/.bitsocial-ai-moderation-audit.jsonl` | Private JSONL verdict audit log path; set to an empty string to disable audit logging | | `rejectDuplicateMedia` | `false` | Reject top-level posts that reuse an image, video, or audio URL from a non-archived post in the same community | | `error` | `Rejected by Bitsocial AI moderation.` | Error shown when content edits are rejected or moderation is unavailable for an edit | Prompt source precedence is `prompt` > `promptPath` > `promptUrl` > built-in fallback. If multiple private prompt sources are configured, the challenge uses the highest-precedence source and emits a warning about the ignored source. Remote prompts must use HTTPS. When `promptBearerToken` is set, it is sent as an `Authorization: Bearer ...` header rather than in the URL. Prefer `.md` or `text/markdown` for human-maintained prompts and `.txt` or `text/plain` for plain text prompts; the model only receives the fetched text, so the file extension itself does not change model behavior. For providers exposing the chat-completions API shape, set both `apiFormat` and `apiUrl`: ```js { name: "@bitsocial/ai-moderation-challenge", options: { branch: "allow", apiFormat: "chat-completions", apiUrl: "https://provider.example/v1/chat/completions", apiKey: "provider-key", model: "provider-model", reasoningEffort: "high" } } ``` OpenAI-compatible APIs are a practical compatibility convention, not a formal open standard. Test custom providers before enabling the challenge on live communities. Provider API keys are only sent to HTTPS endpoints. Keyless HTTP endpoints remain available for trusted local or self-hosted deployments. To enable 5chan-style exact-media rejection, set `rejectDuplicateMedia: "true"` on both the `allow` and `review` challenge entries. PKC challenge option values are strings; leaving this option unset preserves the default and does not perform the deterministic hard-rejection check. ## Settings validation and public options `pkc-js` 0.0.85+ validates `community.settings.challenges[i]` on every community edit, creation, and start. For this challenge that means: - Option keys that are not listed in the table above are rejected as typos by `pkc-js` itself. - The challenge's `validateChallengeSettings` hook rejects the same option errors that would otherwise fail every publication: an `apiUrl` or `triageApiUrl` that is not `http`/`https`, a keyed provider URL that is not HTTPS, a `promptUrl` that is not `https`, an unknown API format, reasoning effort, or `branch`, or a `rejectDuplicateMedia` value other than `true`/`false`. The hook is synchronous and never contacts a provider, so a missing or wrong API key is only discovered when a publication is moderated (fail closed). - If `promptPath` does not exist on the node, the hook logs it through `pkc-logger` but does not reject the settings, so a prompt file that is created later does not block the community. - Rejections fail the offending `community.edit()`; at start they surface as community `error` events with code `ERR_CHALLENGE_SETTINGS_VALIDATION_FAILED` and the community still starts. Existing settings that were silently broken start emitting these errors after upgrading. Every option is private by default. An owner can publish specific options by naming them in `publicOptions`, and `pkc-js` then copies their values into the public `community.challenges[i].publicOptions`. The hook refuses to publish `apiKey`, `triageApiKey`, and `promptBearerToken` because they are credentials. Everything else is the owner's call: publishing `prompt`, `promptUrl`, or `promptPath` is a transparency choice, but it lets users read the moderation prompt and try to game it, and publishing provider URLs, `cachePath`, or `auditLogPath` reveals private node details. Most communities should leave `publicOptions` unset. ```js { name: "@bitsocial/ai-moderation-challenge", options: { apiKey: "sk-...", branch: "allow", rejectDuplicateMedia: "true" }, publicOptions: ["branch", "rejectDuplicateMedia"] } ``` ## Behavior - New comments with verdict `allow` publish normally. - New comments with verdict `review` are sent to pending approval with the redacted model reason attached for the author. - New comments are also sent to pending approval if the model API is unavailable. - When `triageModel` is configured, its `allow` verdict is final and avoids a primary-model call. A triage `review` verdict or triage-provider failure calls the primary reviewer, and only that reviewer's verdict can send content to pending approval. - Each provider request times out after 30 seconds. A triage timeout escalates to the primary reviewer; a primary-reviewer timeout follows the same fail-closed path as other provider failures. - When `fallbackModel` is configured, an HTTP 429 from the primary model is retried once with the fallback model before the publication is sent to pending approval. - Comments sent to pending approval because moderation is unavailable include a generic moderator-visible reason; provider details remain in the private audit log. - Content edits with verdict `review` are rejected until PKC supports pending approval for edits. - Content edits are rejected if the model API is unavailable. - Delete-only edits and non-comment publication types bypass AI moderation. - When `rejectDuplicateMedia` is `"true"`, new top-level posts reuse neither an exact image, video, or audio URL from a non-archived top-level post nor an in-flight media URL in the same community. This option is disabled by default and is configured independently by each community operator. The deterministic check covers every non-archived thread, runs before any model request, and never enters pending approval; URL comparison upgrades HTTP to HTTPS, ignores fragments and default ports, and retains query parameters. - The challenge sends text, title, submission time, link URL/domain/path, URL-path date hints, flags, flairs, community address/title/description, `community.rules`, and a bounded activity-relative list of recent top-level posts for duplicate-thread checks when the local community database is available. - The model payload explicitly labels publication fields as untrusted user content, not instructions. - The challenge does not fetch linked publication media or user-submitted URLs. `promptUrl` is an operator-configured private prompt source, not publication content. - Remote prompts are fetched without following redirects, with a 5 second timeout, capped at 64 KiB, cached in memory for 5 minutes, and reused from the last in-memory copy if a refresh fails. If the first remote prompt fetch fails, moderation fails closed for the allow branch. - Two branch invocations for the same publication reuse one in-process verdict promise. - Successful verdicts are cached in a private JSON file keyed by a SHA-256 hash over primary and triage model/provider config, community context including duplicate-check context, target content, and the final prompt hash. The cache does not store the raw prompt or API keys. - Verdicts are written to a private JSONL audit log with the model stage, model reason, raw publication fields, and hashes/metadata for correlation. The audit log does not store the raw prompt, API keys, prompt URL, or prompt bearer token. ## Moderation Audit Community The challenge writes one private JSONL audit entry per model verdict. To mirror those entries into a Bitsocial community for moderators, create an unnamed local community with a single `question` challenge, store the answer in a private node-local file, and run the publisher script on the node: ```bash node scripts/publish-audit-log-to-community.mjs \ --community 12D3KooW... \ --audit-log ~/.bitsocial-ai-moderation-audit.jsonl \ --challenge-answer-file ~/.bitsocial-ai-moderation-mod-log-password \ --follow ``` The publisher creates a persistent local signer at `~/.bitsocial-ai-moderation-mod-log-signer.json`, stores its read offset in `~/.bitsocial-ai-moderation-mod-log-state.json`, and submits the private challenge answer when publishing. Each mod-log post includes the AI action, verdict reason, matched rule indexes, source community, publication kind, author identifiers, available CIDs, link metadata, content/title, provider/model, cache key, prompt hash, and rule hash. ## Test Coverage The coverage badge reports line coverage generated with `yarn test:coverage`. On pushes to `master`, CI writes a Shields-compatible endpoint payload and publishes it to GitHub Pages. The test suite covers the moderation-critical flow: OpenAI-compatible Responses and chat-completions requests include `community.rules`, triage approvals avoid the primary reviewer, triage reviews and outages escalate, model `review` verdicts fail the `allow` branch and pass the `review` branch used with `pendingApproval`, provider outages and malformed responses route new comments to review, and content edits are rejected on review or outage. ## Publishing The first npm publish must create the package before trusted publishing can be configured: ```bash npm publish --access public ``` After the package exists, configure npm trusted publishing: - Publisher: GitHub Actions - Organization: `bitsocialnet` - Repository: `ai-moderation-challenge` - Workflow filename: `publish.yml` - Environment: leave blank Equivalent npm CLI command: ```bash npm trust github @bitsocial/ai-moderation-challenge --repo bitsocialnet/ai-moderation-challenge --file publish.yml ``` Future releases publish automatically when `package.json` version changes on `master`. The publish workflow skips versions that already exist on npm. ### BitsocialSpamBlocker Repository: bitsocialnet/spam-blocker Source: https://github.com/bitsocialnet/spam-blocker#readme Description: A centralized spam detection service that evaluates publications and provides risk scores to help communities filter spam. This public workspace contains the integration packages: # BitsocialSpamBlocker ## Overview A centralized spam detection service that evaluates publications and provides risk scores to help communities filter spam. This public workspace contains the integration packages: 1. **Challenge Package** (`@bitsocial/spam-blocker-challenge`) - package for community integration 2. **Shared Package** (`@bitsocial/spam-blocker-shared`) - shared schemas and response types used by the public integration surface ## Licensing This monorepo uses package-specific licensing: - `packages/challenge`: `GPL-3.0-or-later`. This is the public challenge package for community integrations. - `packages/shared`: `MIT`. This is the permissive shared-types package intended to be reused by both private services and public integrations. The hosted Bitsocial spam blocker server implementation now lives in a separate private repository and is not included here. The repo root is marked `UNLICENSED` so the workspace metadata does not imply a single open-source license for the whole repository. The official hosted service at `https://spamblocker.bitsocial.net/api/v1` is the default and strongly recommended integration because its centralized history enables cross-community abuse and signer-cluster detection. `serverUrl` remains configurable for compatible independent services, but the official hosted server's proprietary implementation is not distributed by this repository. See [Hosted Spam Blocker Source Policy](https://github.com/bitsocialnet/spam-blocker/blob/HEAD/docs/architecture/hosted-service-source-policy.md) for the rationale, transparency commitments, and conditions for reconsidering this boundary. ## Development Workflow Repo-specific AI workflow guidance lives in: - [`AGENTS.md`](https://github.com/bitsocialnet/spam-blocker/blob/HEAD/AGENTS.md) - [`CLAUDE.md`](https://github.com/bitsocialnet/spam-blocker/blob/HEAD/CLAUDE.md) - [`docs/agent-playbooks/hooks-setup.md`](https://github.com/bitsocialnet/spam-blocker/blob/HEAD/docs/agent-playbooks/hooks-setup.md) - [`docs/agent-playbooks/skills-and-tools.md`](https://github.com/bitsocialnet/spam-blocker/blob/HEAD/docs/agent-playbooks/skills-and-tools.md) The repo is intended to be worked with Corepack-managed Yarn and repo-managed agent hooks. See the playbooks for the recommended setup and verification flow. Quick setup for a fresh machine: 1. `corepack enable` 2. `corepack yarn install` 3. `./scripts/install-default-agent-skills.sh` **Important:** - `packages/shared` defines the public response schemas used by the challenge package and by hosted server integrations. - The hosted server implementation is private; this README documents the public integration contract exposed to the challenge package. ## Repository Structure ``` bitsocial-spam-blocker/ ├── package.json # Root workspace config ├── tsconfig.base.json ├── docs/ # Architecture decisions and agent playbooks ├── packages/ │ ├── challenge/ # package for community owners │ │ └── src/ │ │ └── index.ts # ChallengeFileFactory │ └── shared/ # Shared types │ └── src/types.ts ``` ## API Endpoints ### POST /api/v1/evaluate Evaluate publication risk. The server tracks author history internally, so no completion tokens are needed. Requests are signed by the community signer to prevent abuse (e.g., someone unrelated to the community querying the engine to doxx users). The server validates the request signature and ensures the signer matches the community (for domain addresses, the server resolves the community via `bitsocial.getCommunity` and compares `community.signature.publicKey`). Resolved community public keys are cached in-memory for 12 hours to reduce repeated lookups. The HTTP server initializes a single shared bitsocial instance and only destroys it when the server shuts down. The public challenge package does not call this endpoint from `getChallenge()`. It returns `/api/v1/iframe/:sessionId/lazy#payload=...` instead. The signed payload is stored in the URL fragment, which is not sent on the initial iframe GET; the lazy iframe posts it to `/evaluate` only after the user opens the iframe. **Request Format:** `Content-Type: application/cbor` The request body is CBOR-encoded (not JSON). This preserves `Uint8Array` types during transmission and ensures signature verification works correctly. **Request:** ```typescript // The request wraps the DecryptedChallengeRequestMessageTypeWithcommunityAuthor from bitsocial-js // communityAddress is required; author.community is optional (undefined for first-time publishers) // The signature is created by CBOR-encoding the signed properties, then signing with Ed25519 { challengeRequest: DecryptedChallengeRequestMessageTypeWithcommunityAuthor; sessionId?: string; // Optional client-generated ID for consent-gated lazy iframes evaluationOptions?: { autoAcceptThreshold?: number; autoRejectThreshold?: number; }; timestamp: number; // Unix timestamp (seconds) signature: { signature: Uint8Array; // Ed25519 signature of CBOR-encoded signed properties publicKey: Uint8Array; // 32-byte Ed25519 public key type: "ed25519"; signedPropertyNames: Array<"challengeRequest" | "sessionId" | "evaluationOptions" | "timestamp">; } } ``` **Response:** ```typescript { riskScore: number; // 0.0 to 1.0 explanation?: string; // Human-readable reasoning for the score sessionId: string; challengeUrl: string; // Full URL: https://spamblocker.bitsocial.net/api/v1/iframe/{sessionId} challengeExpiresAt?: number; // Unix timestamp, 1 hour from creation } ``` The response always includes the concrete challenge session URL. For the public challenge package's lazy iframe flow, low-risk and high-risk sessions are handled inside the lazy iframe without redirecting to the full challenge UI. Medium-risk sessions redirect to `challengeUrl`. ### POST /api/v1/challenge/verify Called by the community's challenge code to verify that the user completed the iframe challenge. The server tracks challenge completion state internally - no token is passed from the user. **Request must be signed by the community** (same signing mechanism as /evaluate), using the same signing key that was used for the evaluate request. **Request Format:** `Content-Type: application/cbor` **Request:** ```typescript { sessionId: string; // The sessionId from the /evaluate response timestamp: number; // Unix timestamp (seconds) signature: { signature: Uint8Array; // Ed25519 signature of CBOR-encoded signed properties publicKey: Uint8Array; // 32-byte Ed25519 public key type: "ed25519"; signedPropertyNames: ["sessionId", "timestamp"]; } } ``` **Response:** ```typescript { success: boolean; error?: string; // If success is false // The following fields are returned on success, allowing the challenge // code to make additional filtering decisions ipRisk?: number; // 0.0 to 1.0, risk score based on IP analysis ipAddressCountry?: string; // ISO 3166-1 alpha-2 country code (e.g., "US", "RU") challengeType?: string; // What challenge was sent (e.g., "turnstile", "hcaptcha") ipTypeEstimation?: string; // "residential" | "vpn" | "proxy" | "tor" | "datacenter" | "unknown" } ``` ### GET /api/v1/iframe/:sessionId/lazy Serves the consent-gated bootstrap iframe used by the public challenge package. This route does not create a challenge session by itself. After the iframe opens, its script reads the signed CBOR payload from the URL fragment, posts it to `/api/v1/evaluate`, then either completes/fails immediately or redirects to `/api/v1/iframe/:sessionId`. ### GET /api/v1/iframe/:sessionId Serves the iframe challenge page. The iframe uses an **OAuth-first** flow where OAuth is the primary trust signal and CAPTCHA is a fallback. - **OAuth providers** (primary): GitHub, Google, Twitter, Yandex, TikTok, Discord, Reddit - **CAPTCHA provider** (fallback): Cloudflare Turnstile > **Privacy note**: For OAuth providers, the server only verifies successful authentication - it does NOT share account identifiers (username, email) with the community. The hosted operator stores IP evidence for abuse prevention, including signer-cluster detection; only the country code and risk result are shared with the community, never the raw IP address. **Iframe logic (OAuth-first):** When OAuth providers are configured, the iframe shows OAuth buttons as the primary challenge: 1. **Initial view**: OAuth sign-in buttons. If CAPTCHA alone can pass at this score level, a "I don't have a social account" link is also shown. 2. **After first OAuth**: If `riskScore × oauthMultiplier < passThreshold` → session completes. Otherwise, "Additional verification needed" view shows remaining providers and optional CAPTCHA. 3. **CAPTCHA fallback**: Shown when the user clicks "I don't have a social account". If OAuth was already completed, the combined multiplier (OAuth × CAPTCHA) is applied. When no OAuth is configured, a turnstile-only CAPTCHA iframe is served. **Challenge completion flow:** 1. User signs in via OAuth (or solves CAPTCHA fallback) 2. Server applies score adjustment and determines if session passes 3. If more verification needed, iframe transitions to "need more" view 4. Once passed, iframe shows "Verification complete!" 5. The user clicks "done" in their bitsocial client (the client provides this button outside the iframe) 6. The client sends a `ChallengeAnswer` with an empty string to the community 7. The community's challenge code calls `/api/v1/challenge/verify` to check if the session is completed ### POST /api/v1/challenge/complete Called by the iframe after the user solves the CAPTCHA (as a fallback in the OAuth-first flow). Validates the Turnstile response, then applies score adjustment to decide whether the session passes. **Request:** ```typescript { sessionId: string; challengeResponse: string; // Token from the challenge provider challengeType?: string; // e.g., "turnstile" (default) } ``` **Response:** ```typescript { success: boolean; error?: string; // Error message on failure passed?: boolean; // Whether the challenge is fully passed (session completed) oauthRequired?: boolean; // Whether OAuth is required (CAPTCHA alone is not enough) } ``` **Score adjustment logic:** After validating the CAPTCHA, the server checks if OAuth was already completed. If so, the combined multiplier is used: `adjustedScore = riskScore × oauthMultiplier × captchaMultiplier`. Otherwise: `adjustedScore = riskScore × captchaMultiplier`. If `adjustedScore < challengePassThreshold`, the session is marked `completed` and `passed: true` is returned. Otherwise, the CAPTCHA is marked complete but the session stays `pending`, and `passed: false, oauthRequired: true` is returned. ### OAuth Routes **GET /api/v1/oauth/:provider/start?sessionId=...** — Initiates the OAuth flow. Generates state, stores it in the database, and redirects the user to the OAuth provider's authorization page. **GET /api/v1/oauth/:provider/callback** — OAuth callback handler. Exchanges the authorization code for a token, retrieves the user identity, then applies score adjustment: - **First OAuth**: If `riskScore × oauthMultiplier < passThreshold` → session completed. Otherwise, marks `oauthCompleted` and session stays pending ("need more" state). - **Second OAuth**: Must be from a different provider. Applies `riskScore × oauthMultiplier × secondOauthMultiplier`. If below threshold → session completed. - Multiple OAuth identities are accumulated as a JSON array in the session's `oauthIdentity` field. **GET /api/v1/oauth/status/:sessionId** — Polling endpoint used by the iframe to check OAuth status. Returns `{ completed, oauthCompleted, needsMore, firstProvider, status }`. ## Challenge Flow (Detailed) The challenge flow uses **server-side state tracking** - no tokens are passed from the iframe to the user's client. This matches the standard bitsocial iframe challenge pattern (used by mintpass and others). **OAuth is the primary challenge.** The iframe shows OAuth sign-in buttons first. CAPTCHA is available as a fallback for users without social accounts. After the user completes verification, the server adjusts the risk score. If the adjusted score is below the pass threshold, the session completes. For high-risk users, additional verification (second OAuth from a different provider, or CAPTCHA) may be required. ``` getChallenge() -> lazy iframe URL (no spam blocker server request) | v User opens iframe -> lazy iframe posts signed payload to /evaluate -> riskScore | ├─ < autoAcceptThreshold -> auto_accept (verify succeeds immediately) ├─ >= autoRejectThreshold -> auto_reject (verify fails immediately) └─ between -> create session (store riskScore), return challengeUrl | v Iframe serves OAuth buttons (primary) + optional CAPTCHA fallback link | ├─ User signs in via OAuth -> callback applies score adjustment | | | ├─ riskScore x oauthMultiplier < passThreshold? | | YES -> mark "completed" -------------------------> /verify -> success | | | └─ NO -> mark oauthCompleted, session stays "pending" | Iframe shows "need more" view | | | ├─ User signs in with 2nd OAuth (different provider) | | -> riskScore x oauthMult x 2ndOauthMult < threshold? | | YES -> completed ------------------> /verify -> success | | | └─ User completes CAPTCHA | -> riskScore x oauthMult x captchaMult < threshold? | YES -> completed ------------------> /verify -> success | └─ User clicks "I don't have a social account" -> CAPTCHA fallback | ├─ riskScore x captchaMultiplier < passThreshold? | YES -> mark "completed" -------------------------> /verify -> success | └─ NO -> mark captchaCompleted, return { oauthRequired: true } Iframe redirects back to OAuth view ``` ``` 1. Client sends ChallengeRequest to the community. 2. Community challenge code returns a lazy spam blocker iframe URL with a signed payload in the URL fragment. 3. Client asks the user before opening the iframe. 4. After the user opens the iframe, the iframe posts the signed payload to /evaluate. 5. /evaluate creates the session and either completes, fails, or returns the full challengeUrl. 6. Medium-risk sessions redirect to /iframe/:sessionId for OAuth and/or CAPTCHA. 7. When done, the iframe posts a ChallengeAnswer with an empty string. 8. Community verify("") calls /challenge/verify with the pre-generated sessionId. 9. The community applies post-challenge IP filters and accepts or rejects the publication. ``` ## Risk Score The risk score is a value between 0.0 and 1.0 that indicates the likelihood a publication is spam or malicious. It's calculated as a weighted combination of multiple factors including account age, karma, author reputation, content analysis, velocity, IP intelligence, and signer-cluster behavior. Detailed risk-scoring implementation notes live with the private server codebase. This public repo documents the exposed API contract and the public challenge/shared packages. ### Signer-cluster Sybil resistance The hosted service links distinct signer public keys first observed on the same confirmed user-origin IP. This makes disposable Bitsocial accounts less useful for evading a community ban, manufacturing a conversation, or voting on related accounts' content. - A few signers are allowed to accommodate users who cannot yet sync their Bitsocial account across devices. The first three signers in the rolling 30-day window are normally treated as legitimate multi-device use. - Additional signers progressively raise risk and appear as operator evidence. Rapid account churn, ban evasion, repeated failures, or replies/votes between signers linked to the same IP can require stronger verification. - Severe rapid or compound abuse can cause a temporary IP-level rejection while the rolling evidence remains active. Operators may also apply a persistent manual IP block. This is a centralized, service-local defense rather than a protocol-wide Bitsocial identity or ban. Shared households, offices, and carrier networks can legitimately reuse an IP, so the signal is graduated and is not treated as proof that multiple signers are one person. The hosted challenge UI warns users about the policy. ## Indexer The server includes a background indexer that crawls the Bitsocial network to build author reputation data. It: - Indexes communities and their comments/posts - Follows `author.previousCommentCid` chains to discover new communities - Tracks modQueue to see which authors get accepted/rejected - Detects bans/removals by monitoring CommentUpdate availability - Provides network-wide author reputation data for risk scoring Indexer implementation details live with the private server codebase. **Tier Thresholds (configurable per community via challenge options):** - `riskScore < autoAcceptThreshold` → Complete after iframe evaluation (no OAuth/CAPTCHA) - `autoAcceptThreshold <= riskScore < oauthSufficientThreshold` → One OAuth is sufficient (`oauth_sufficient`) - `oauthSufficientThreshold <= riskScore < autoRejectThreshold` → OAuth + more needed (`oauth_plus_more`) - `riskScore >= autoRejectThreshold` → Reject after iframe evaluation **Score Adjustment (configurable on server):** OAuth is the primary trust signal. CAPTCHA is a fallback for users without social accounts. | Path | Formula | Default | Pass if | | ------------------------ | ----------------------------------- | ----------------- | ------------------------ | | OAuth alone | score × oauthScoreMultiplier | score × 0.6 | < challengePassThreshold | | CAPTCHA alone (fallback) | score × captchaScoreMultiplier | score × 0.7 | < challengePassThreshold | | OAuth + second OAuth | score × oauthMult × secondOauthMult | score × 0.6 × 0.5 | < challengePassThreshold | | OAuth + CAPTCHA | score × oauthMult × captchaMult | score × 0.6 × 0.7 | < challengePassThreshold | With default values (threshold 0.4): - One OAuth sufficient when raw score < ~0.67 - CAPTCHA alone sufficient when raw score < ~0.57 - OAuth + second OAuth sufficient when raw score < ~1.33 (all non-auto-rejected pass) - OAuth + CAPTCHA sufficient when raw score < ~0.95 (most non-auto-rejected pass) ## Dynamic Rate Limiting An opt-in pre-check that hard-rejects publications (HTTP 429) when an author exceeds their budget. This runs before risk scoring and prevents manual spammers who solve CAPTCHAs from posting at high rates. **Enabling:** Pass `rateLimitConfig: {}` in `RouteOptions` to enable with defaults. Omit it to disable entirely. **Dynamic budgets:** Each author gets a budget multiplier based on `ageFactor × reputationFactor` (clamped 0.25–5.0): | Account Age | ageFactor | | Condition | reputationFactor | | -------------------- | --------- | --- | ---------------------- | ---------------- | | No history / < 1 day | 0.5 | | Any active bans | 0.5 | | 1–7 days | 0.75 | | Removal rate > 30% | 0.5 | | 7–30 days | 1.0 | | Removal rate 15–30% | 0.75 | | 30–90 days | 1.5 | | No history or < 15% | 1.0 | | 90–365 days | 2.0 | | < 5% AND > 10 comments | 1.25 | | > 365 days | 3.0 | | | | **Base limits (at 1.0× multiplier), effective = `max(1, floor(base × multiplier))`:** | Type | Hourly | Daily | | ------------- | ------ | ------- | | post | 4 | 20 | | reply | 6 | 60 | | vote | 10 | 200 | | **aggregate** | **40** | **250** | Check order: per-type hourly → per-type daily → aggregate hourly → aggregate daily. Only user-generated content (posts, replies, votes) is rate-limited. The challenge package accepts community-level actions (commentEdit, commentModeration, communityEdit) without calling the evaluate endpoint since they don't require spam detection. ## Challenge Verification Challenge completion is tracked **server-side** in the database - no tokens are passed to the user's client. When a user completes the iframe challenge: 1. The iframe shows OAuth sign-in buttons; user signs in with a provider 2. The OAuth callback applies score adjustment (`riskScore × oauthMultiplier`) 3. If the adjusted score is below `challengePassThreshold` → session marked `completed` 4. If not → `oauthCompleted` is set, iframe shows "need more" view with remaining providers and optional CAPTCHA 5. User completes second OAuth (different provider) or CAPTCHA → combined multiplier applied → session marked `completed` 6. Alternatively, user can use CAPTCHA fallback from the start ("I don't have a social account") 7. The user clicks "done" in their bitsocial client 8. The client sends a `ChallengeAnswer` with an empty string to the community 9. The community's challenge code calls `/api/v1/challenge/verify` with the `sessionId` 10. The server checks `session.status === "completed"` and returns success + IP intelligence **Session expiry:** 1 hour from creation ## Database Schema (SQLite + better-sqlite3) **Tables:** Author columns store the full `author` object from each publication (for example, `DecryptedChallengeRequestMessageTypeWithcommunityAuthor.comment.author`). ### `comments` Stores comment publications for analysis and rate limiting. - `sessionId` TEXT PRIMARY KEY (foreign key of challengeSessions) - `author` TEXT NOT NULL -- is actually a JSON - `communityAddress` TEXT NOT NULL - `parentCid` TEXT (null for posts, set for replies) - `content` TEXT - `link` TEXT - `linkWidth` INTEGER - `linkHeight` INTEGER - `postCid` TEXT - `signature` TEXT NOT NULL - `title` TEXT - `timestamp` INTEGER NOT NULL - `linkHtmlTagName` TEXT - `flair` TEXT - `spoiler` INTEGER (BOOLEAN 0/1) - `protocolVersion` TEXT NOT NULL - `nsfw` INTEGER (BOOLEAN 0/1) - `receivedAt` INTEGER NOT NULL ### `votes` Stores vote publications. - `sessionId` TEXT PRIMARY KEY (foreign key of challengeSessions) - `author` TEXT NOT NULL -- is actually a json - `communityAddress` TEXT NOT NULL - `commentCid` TEXT NOT NULL - `signature` TEXT NOT NULL - `protocolVersion` TEXT NOT NULL - `vote` INTEGER NOT NULL (-1, 0 or 1) - `timestamp` INTEGER NOT NULL - `receivedAt` INTEGER NOT NULL ### `challengeSessions` Tracks challenge sessions. Sessions are kept permanently for historical analysis. Internal timestamps (completedAt, expiresAt, receivedChallengeRequestAt, authorAccessedIframeAt) are in milliseconds. - `sessionId` TEXT PRIMARY KEY -- UUID v4 - `communityPublicKey` TEXT - `status` TEXT DEFAULT 'pending' (pending, completed, failed) - `completedAt` INTEGER - `expiresAt` INTEGER NOT NULL - `receivedChallengeRequestAt` INTEGER NOT NULL - `authorAccessedIframeAt` INTEGER -- when did the author access the iframe? - `oauthIdentity` TEXT -- format: "provider:userId" or JSON array '["provider:userId", ...]' - `challengeTier` TEXT -- 'oauth_sufficient' or 'oauth_plus_more' (determined by score thresholds) - `captchaCompleted` INTEGER DEFAULT 0 -- 1 if CAPTCHA portion completed - `oauthCompleted` INTEGER DEFAULT 0 -- 1 if first OAuth completed - `riskScore` REAL -- the risk score at evaluation time (used for score adjustment after OAuth/CAPTCHA) ### `ipRecords` Stores raw IP addresses associated with authors (captured via iframe). One record per challenge. - `sessionId` TEXT NOT NULL (foreign key to challengeSessions.sessionId) PRIMARY KEY - `ipAddress` TEXT NOT NULL -- ip address string representation - `isVpn` INTEGER (BOOLEAN 0/1) - `isProxy` INTEGER (BOOLEAN 0/1) - `isTor` INTEGER (BOOLEAN 0/1) - `isDatacenter` INTEGER (BOOLEAN 0/1) - `countryCode` TEXT -- ISO 3166-1 alpha-2 country code - `timestamp` INTEGER NOT NULL -- when did we query the ip provider ### `oauthStates` Ephemeral table for CSRF protection during OAuth flow. Internal timestamps (createdAt, expiresAt) are in milliseconds. - `state` TEXT PRIMARY KEY - `sessionId` TEXT NOT NULL (foreign key to challengeSessions) - `provider` TEXT NOT NULL -- 'github', 'google', 'twitter', etc. - `codeVerifier` TEXT -- PKCE code verifier (required for google, twitter) - `createdAt` INTEGER NOT NULL - `expiresAt` INTEGER NOT NULL ## Challenge Code Implements pkc-js `ChallengeFileFactory`: ```typescript // Usage in community settings { "challenges": [{ "name": "@bitsocial/spam-blocker-challenge", "options": { "serverUrl": "https://spamblocker.bitsocial.net/api/v1", "autoAcceptThreshold": "0.2", "autoRejectThreshold": "0.8", "countryBlacklist": "RU,CN,KP", "blockVpn": "true", "blockTor": "true" }, "exclude": [ { "role": ["owner", "admin", "moderator"] }, { "postScore": 100 } ] }] } ``` When calling `/api/v1/evaluate`, the `author.community` field in the publication (e.g., `challengeRequest.comment.author.community`) may be `undefined` for first-time publishers who have never posted in the community before. The community populates this field from its internal database of author history, so new authors won't have it set. ### Configuration Options (Challenge Package) | Option | Default | Description | | --------------------- | ------------------------------------------ | ------------------------------------------------------------------------------ | | `serverUrl` | `https://spamblocker.bitsocial.net/api/v1` | URL of the BitsocialSpamBlocker server (must be http/https) | | `autoAcceptThreshold` | `0.2` | Complete verification after iframe evaluation when risk is below this score | | `autoRejectThreshold` | `0.8` | Reject after iframe evaluation when risk is above this score | | `countryBlacklist` | `""` | Comma-separated ISO 3166-1 alpha-2 country codes to block (e.g., `"RU,CN,KP"`) | | `maxIpRisk` | `1.0` | Reject if ipRisk from /verify exceeds this threshold | | `blockVpn` | `false` | Reject publications from VPN IPs (`true`/`false` only) | | `blockProxy` | `false` | Reject publications from proxy IPs (`true`/`false` only) | | `blockTor` | `false` | Reject publications from Tor exit nodes (`true`/`false` only) | | `blockDatacenter` | `false` | Reject publications from datacenter IPs (`true`/`false` only) | **Post-challenge filtering:** After a user completes a challenge, the `/verify` response includes IP intelligence data. The challenge code uses the above options to reject publications even after successful challenge completion (e.g., if the user is from a blacklisted country or using a VPN). **Error Handling:** If the server is unreachable, the challenge code returns a failed challenge result instead of silently accepting or rejecting user-generated content. Community-level actions are accepted locally without contacting the server. **Privacy of options:** The `options` object (including `serverUrl` and all threshold/filtering settings) is **not** exposed in the public `community.challenges` IPFS record. bitsocial-js strips `options` when computing the public `communityChallenge` from `communityChallengeSetting`, so only `type`, `description`, and `exclude` are published. This means the server URL, thresholds, and filtering rules remain private to the community operator. ### Server Configuration (separate from challenge) These settings are configured on the HTTP server, not in the challenge package: **Required:** - `DATABASE_PATH`: Path to the SQLite database file. Use `:memory:` for in-memory. **Challenge providers:** - `TURNSTILE_SITE_KEY`: Cloudflare Turnstile site key - `TURNSTILE_SECRET_KEY`: Cloudflare Turnstile secret key - `BASE_URL`: Base URL for OAuth callbacks (e.g., `https://spamblocker.bitsocial.net`) **IP Intelligence:** - `IPAPI_KEY`: ipapi.is API key for IP intelligence lookups (optional — works without key) **Challenge tier thresholds:** - `AUTO_ACCEPT_THRESHOLD`: Complete immediately after evaluation below this score (default: 0.2) - `OAUTH_SUFFICIENT_THRESHOLD`: Scores between autoAccept and this pass with one OAuth (default: 0.4) - `AUTO_REJECT_THRESHOLD`: Reject immediately after evaluation at or above this score (default: 0.8) **Score adjustment (OAuth-first model):** - `OAUTH_SCORE_MULTIPLIER`: Multiplier applied after first OAuth, in (0, 1] (default: 0.6) - `SECOND_OAUTH_SCORE_MULTIPLIER`: Multiplier applied after second OAuth from different provider, in (0, 1] (default: 0.5) - `CAPTCHA_SCORE_MULTIPLIER`: Multiplier applied after CAPTCHA (fallback), in (0, 1] (default: 0.7) - `CHALLENGE_PASS_THRESHOLD`: Adjusted score must be below this, in (0, 1) (default: 0.4) **OAuth providers** (each requires both CLIENT_ID and CLIENT_SECRET): - `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` - `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` - `TWITTER_CLIENT_ID` / `TWITTER_CLIENT_SECRET` - `YANDEX_CLIENT_ID` / `YANDEX_CLIENT_SECRET` - `TIKTOK_CLIENT_ID` / `TIKTOK_CLIENT_SECRET` - `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` - `REDDIT_CLIENT_ID` / `REDDIT_CLIENT_SECRET` **Risk factor disabling:** - `DISABLED_RISK_FACTORS`: Comma-separated list of risk factor names to disable. Disabled factors get `weight=0` and their weight is redistributed to remaining factors. Valid values: `commentContentTitleRisk`, `commentUrlRisk`, `velocityRisk`, `accountAge`, `karmaScore`, `ipRisk`, `signerClusterRisk`, `networkBanHistory`, `modqueueRejectionRate`, `networkRemovalRate`, `socialVerification`, `walletVerification`. Example: `DISABLED_RISK_FACTORS=walletVerification` **Other:** - `PORT`: Server port (default: 3000) - `HOST`: Server host (default: 0.0.0.0) - `LOG_LEVEL`: Set to `silent` to disable logging - `PKC_RPC_URL`: PKC RPC URL for community resolution - `ALLOW_NON_DOMAIN_COMMUNITIES`: Set to `true` to allow non-domain community addresses ## Key Design Decisions - **Database:** SQLite with better-sqlite3, no ORM - **Content Analysis:** Server-side setting, enabled by default - **Primary Challenge Provider:** Cloudflare Turnstile (free, privacy-friendly) - **Challenge Model:** CAPTCHA-first with score-based OAuth gating (CAPTCHA always required; OAuth only if score remains too high after adjustment) - **OAuth Library:** Arctic (lightweight, supports many providers) - **Error Handling:** Return failed challenge results on server errors (no silent failures) - **IP Storage:** Raw IPs stored (not hashed) for accurate analysis - **IP Intelligence:** ipapi.is (external HTTP API, best-effort, works without API key) - **Ephemeral Sessions:** Challenge sessions auto-purge after 1 hour ## Privacy Considerations - Raw IPs are stored for spam detection purposes - Content analysis is performed on the server - IP intelligence lookups are sent to ipapi.is when enabled - OAuth identity (provider:userId) is stored server-side but never shared with communities - All data is visible to the server operator - Public integration contract documented here; hosted implementation is private - Explanation field shows reasoning for scores ## Known Limitations - IP intelligence fields are best-effort estimates and can be wrong (e.g., VPNs, residential IPs, or misclassification) - Treat IP intelligence as informational and use it only for rejection decisions - IP intelligence fields are optional and may be removed from the engine response in the future; challenge code only applies IP filtering options when they are present - IP-based options are intentionally rejection-only; we do not support IP-derived auto-approval (e.g., a country whitelist), because it is easy to game and can be used to flood a community - IP-linked signer clusters are probabilistic: shared networks can group legitimate users, while attackers can rotate IPs or use proxies. The allowance and compound-signal escalation reduce false positives but cannot eliminate them. ## Verification Plan 1. Build the public packages with `corepack yarn build` 2. Type-check the public packages with `corepack yarn type-check` 3. Run the challenge-package tests with `corepack yarn test` 4. Integrate the challenge package with a server instance that implements the API contract documented in this README 5. Verify end-to-end flow against a local or hosted private server deployment ## Reference Files - bitsocial-js challenge example: `pkc-js/src/runtime/node/community/challenges/bitsocial-js-challenges/captcha-canvas-v3/index.ts` - bitsocial-js schemas: `pkc-js/src/community/schema.ts` - bitsocial-js challenge orchestration: `pkc-js/src/runtime/node/community/challenges/index.ts` - MintPass iframe challenge: https://github.com/bitsociallabs/mintpass/tree/master/challenge ### @bitsocial/r9k-challenge Repository: bitsocialnet/r9k-challenge Source: https://github.com/bitsocialnet/r9k-challenge#readme Description: Robot9000 style originality challenge for Bitsocial communities. # @bitsocial/r9k-challenge Robot9000-style originality challenge for Bitsocial communities. This package runs on a Bitsocial community owner node as a deterministic Bitsocial challenge. It is not specific to 5chan; any Bitsocial community can use it when the operator wants Robot9000-style anti-repost behavior. It does not use AI. It scans the community owner's local comments database for exact text reposts after Robot9000 normalization and applies escalating temporary bans for failed attempts. Robot9000 is based on the anti-repost idea originally described by Randall Munroe in [ROBOT9000 and #xkcd-signal: Attacking Noise in Chat](https://blog.xkcd.com/2008/01/14/robot9000-and-xkcd-signal-attacking-noise-in-chat/). ## Installation Run this on the Bitsocial node that owns the community: ```bash bitsocial challenge install @bitsocial/r9k-challenge ``` This challenge is listed in the Bitsocial project directory under anti-spam: https://bitsocial.net/projects?category=anti-spam ## Configuration Add the challenge to the community's `settings.challenges`: ```js [ { name: "@bitsocial/spam-blocker-challenge" }, { name: "@bitsocial/r9k-challenge" }, ]; ``` Default options implement Robot9000-style behavior: | Option | Default | Behavior | | ----------------------------------- | --------------------------------------- | ------------------------------------------------------------------- | | `statePath` | `~/.bitsocial-r9k-challenge-state.json` | Private JSON state for temporary bans and accepted-hash race guards | | `minimumOriginalContentLength` | `16` | Requires this many normalized text characters | | `transgressionDecayIntervalSeconds` | `86400` | Forgives one transgression per day | | `penaltyBaseSeconds` | `2` | Temporary ban duration is `2^n` seconds | | `maxPenaltySeconds` | empty | No cap by default | | `blockUnicode` | `true` | Rejects non-ASCII text | | `stripBacklinks` | `true` | Ignores numeric backlinks like `>>123` | | `requireText` | `true` | Rejects posts without title/body text | | `error` | `Rejected by Robot9000.` | Error prefix shown to users | Posts require enough original content to avoid low-effort duplicates. This package uses `16` normalized characters as its default and keeps it configurable per board. ## Behavior - Exact normalized text reposts are rejected by scanning the owner node's `comments` SQLite table during `getChallenge()`. - Numeric backlinks like `>>1` do not count toward originality. - Images, media links, and URLs are not included in the originality hash. - Unicode is rejected by default. - Posts need text; image-only posts fail. - A failed originality attempt temporarily bans the author for `2^n` seconds, where `n` is the current transgression count. - The transgression count decays by one every `transgressionDecayIntervalSeconds`. - State stores SHA-256 hashes for newly accepted text plus temporary-ban counters, not raw post text. Existing-board originality comes from the local database scan. ## Ban Semantics Bitsocial exposes `author.community.banExpiresAt` for moderator bans, but a challenge failure happens before the offending comment is accepted and therefore has no accepted `commentCid` to moderate. This package enforces the same timed-ban behavior inside the challenge state and rejects the author's later challenge requests until `banExpiresAt`. If Bitsocial later exposes a safe challenge-side author-ban API for rejected publications, this package can map the same penalty state to native `banExpiresAt`. ## Development ```bash corepack yarn install corepack yarn type-check corepack yarn test corepack yarn test:coverage corepack yarn build ``` ## Test Coverage The test suite covers Robot9000 normalization, minimum text checks, Unicode blocking, image/link exclusion, escalating and decaying temporary bans, content edits, missing database fail-closed behavior, and duplicate detection against both the challenge state file and a real in-memory SQLite `comments`/`commentUpdates` database. The coverage badge reports line coverage generated with `yarn test:coverage`. On pushes to `master`, CI writes a Shields-compatible endpoint payload and publishes it to GitHub Pages. These tests exercise the challenge package directly. They do not start a full Bitsocial node or publish over the network. ## Publishing Create the GitHub release and changelog with release-it: ```bash corepack yarn release 0.1.0 ``` The command expects the current branch to have an upstream and needs a GitHub token that can create releases. It writes `CHANGELOG.md`, creates a `v0.1.0` tag, and opens the GitHub release. The first npm publish must create the package before trusted publishing can be configured: ```bash npm publish --access public ``` After the package exists, configure npm trusted publishing: - Publisher: GitHub Actions - Organization: `bitsocialnet` - Repository: `r9k-challenge` - Workflow filename: `publish.yml` - Environment: leave blank Equivalent npm CLI command: ```bash npm trust github @bitsocial/r9k-challenge --repo bitsocialnet/r9k-challenge --file publish.yml ``` Future releases publish automatically when `package.json` version changes on `master`. The publish workflow skips versions that already exist on npm. ### @bitsocial/wordfilter-challenge Repository: bitsocialnet/wordfilter-challenge Source: https://github.com/bitsocialnet/wordfilter-challenge#readme Description: A pkc js challenge that makes community wordfilters a real rule instead of a cosmetic display filter. It implements the wordfilter/v1 contract, which is what publishing clients actually code against. # @bitsocial/wordfilter-challenge A [pkc-js](https://github.com/pkcprotocol/pkc-js) challenge that makes community wordfilters a real rule instead of a cosmetic display filter. It implements the [`wordfilter/v1` contract](#the-wordfilterv1-contract), which is what publishing clients actually code against. > **Status: implemented.** This README is the spec, and `src/` follows it. Tracked in [pkc-js#281](https://github.com/pkcprotocol/pkc-js/issues/281). ## What it does A community configures a list of replacements, for example `cloud` becomes `butt`. Publishing clients apply those replacements before signing, so the signed comment, its CID, and what every client renders all contain `butt`. The original text does not exist anywhere. The community node does not do the replacing. It only checks: if a publication still contains a filtered word, it is rejected. ## Why clients have to do the replacing This is the part that surprises people coming from imageboard software, so it is worth being explicit. On a traditional imageboard the server owns the post text and rewrites it freely at submission time. [jschan](https://gitgud.io/fatchan/jschan/-/blob/master/lib/post/filteractions.js) mutates `req.body` before the insert; vichan calls `wordfilters($post['body'])` in `post.php` before `body_nomarkup` is even derived. Neither keeps the pre-filter original. pkc-js cannot work that way. The author signs the publication, and the signature covers `content`. If the community rewrote `content` afterwards, the signature would no longer verify and every client would reject the comment as forged. So the replacement has to happen on the side holding the signing key, which is the publishing client. The community's only available action is to accept or reject. In other words this package does not implement a wordfilter, it implements **a proof that a wordfilter was applied**. A client that does not implement the replacement cannot publish unfiltered text. It simply gets rejected, which is the intended failure mode. ## The `wordfilter/v1` contract A publishing client has to recognise "this community wants word replacements applied before I sign" from the published community record alone. The record deliberately does not say which challenge produced it: `path`, `name` and `options` are all stripped when `community.settings.challenges[i]` becomes the public `community.challenges[i]`. Only `publicOptions`, the subset the owner opted into publishing, survives. So the signal a client keys off is an option key that names a **contract** rather than a package: | Public option | Required | Meaning | |---|---|---| | `wordfilter/v1/rules` | yes | JSON array of `{ src, dst }`. Its presence is what identifies the contract | | `wordfilter/v1/fieldNames` | no | JSON array of dot-notation paths, each starting with the publication type. Defaults to the list under [Default fields](#default-fields) | This package is one implementation. Anything that publishes `wordfilter/v1/rules` with the semantics below claims the contract, and a client written against it keeps working with implementations that did not exist when the client was written. Keying on a package name instead, whether through a hardcoded `@bitsocial/wordfilter-challenge` or a generic `wordfilters` option, would enshrine one implementation in every UI and lock out every fork, competitor and in-house variant that behaves identically. See [issue #1](https://github.com/bitsocialnet/wordfilter-challenge/issues/1). The namespace covers exactly what a publishing client must read, and nothing else. `error` is not in it, because no client reads `error`: the community returns it in the rejection. An implementation is free to name or shape that option however it likes. `wordfilter/v2/rules` would be a different key. A client that understands both reads both, and a community can publish both during a transition without either side guessing. ### Default fields When a community does not publish `wordfilter/v1/fieldNames`, both sides check this list. Every path starts with the publication type, followed by the field's path inside that publication exactly as it is on the wire. | Publication type | Default paths | |---|---| | `comment` | `comment.content`, `comment.title`, `comment.author.displayName` | | `commentEdit` | `commentEdit.content`, `commentEdit.reason`, `commentEdit.author.displayName` | | `vote` | `vote.author.displayName` | The defaults cover what an ordinary author publishes. Moderator and owner text is not in them: a moderation's reason and a community edit's title and description can be filtered by naming their paths in `wordfilter/v1/fieldNames`, as `commentModeration.commentModeration.reason`, `communityEdit.communityEdit.title` and `communityEdit.communityEdit.description`. The doubled segment is the wire shape, not a typo: the publication type is `commentModeration`, and pkc-js puts the moderation's fields under a `commentModeration` property of it; likewise for `communityEdit`. Naming the publication type in every path is deliberate. `content` on a comment and `content` on a comment edit are the same field today, but nothing guarantees they stay that way, and a bare `content` could not tell them apart. A path whose first segment is not one of the five publication types is rejected by `validateChallengeSettings`, and by `getChallenge` for a config that predates that check, because it would match nothing on any request and the wordfilter would have quietly stopped filtering. ## For UI client developers This is the section that matters if you are building a frontend that publishes to communities using this contract. ### 1. Read the rules off the community Collect the rules from every challenge on the community that publishes `wordfilter/v1/rules`, in challenge order. A community may have more than one. ```js const isRule = (rule) => typeof rule?.src === "string" && typeof rule?.dst === "string"; const isFieldName = (name) => typeof name === "string"; function getWordfilterConfigs(community) { return (community.challenges ?? []) .filter((challenge) => challenge.publicOptions?.["wordfilter/v1/rules"]) .map((challenge) => { try { const rules = JSON.parse(challenge.publicOptions["wordfilter/v1/rules"]); const fieldNames = challenge.publicOptions["wordfilter/v1/fieldNames"] ? JSON.parse(challenge.publicOptions["wordfilter/v1/fieldNames"]) : [ "comment.content", "comment.title", "comment.author.displayName", "commentEdit.content", "commentEdit.reason", "commentEdit.author.displayName", "vote.author.displayName" ]; // JSON.parse is happy to hand back null, an object or an array of numbers, and // applyWordfilters would throw on any of them. Treat the wrong shape like bad JSON. if (!Array.isArray(rules) || !rules.every(isRule)) return undefined; if (!Array.isArray(fieldNames) || !fieldNames.every(isFieldName)) return undefined; return { rules, fieldNames }; } catch { // A rule set you cannot parse is skipped, never fatal. Worst case the community // rejects the publication with a readable error. return undefined; } }) .filter(Boolean); } ``` Note what this function does not do: it never looks at which challenge implementation produced the options, because the record does not say and it does not need to. Skip anything you cannot parse, and anything that parses to the wrong shape. Never throw, and never refuse to load the community: a malformed rule set must cost the filter, not the whole board. This package's own `validateChallengeSettings` will not let a community publish a malformed rule set, but the contract is open to other implementations and your client cannot know which one it is talking to. ### 2. Apply the rules, looping until the text stops changing **Do not apply the rules once.** A replacement can create a new match by joining with the text around it. With rules `[{src: "ab", dst: "c"}, {src: "x", dst: "b"}]`, the input `"ax"` becomes `"ab"` in a single pass, which still contains a filtered word, and the community will reject it. Loop until the output is stable: ```js const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // The `() => dst` replacer, not a plain `dst`: `String.replace` reads `$&` and friends out of a string // replacement, and rules are literal on both sides. const applyOnce = (text, rules) => rules.reduce((acc, { src, dst }) => acc.replace(new RegExp(escapeRegExp(src), "gi"), () => dst), text); export function applyWordfilters(text, rules, maxPasses = 8) { let out = text; for (let pass = 0; pass < maxPasses; pass++) { const next = applyOnce(out, rules); if (next === out) return out; out = next; } throw new Error("wordfilter rules did not stabilise"); } ``` Stable output contains no filtered word by definition, which is exactly what the community checks for. Copying that function is the expected path: your client sees the challenge through `community.challenges` and has no reason to depend on the package that produced it. If you do already bundle npm packages, the same function is exported as `applyWordfilters` from `@bitsocial/wordfilter-challenge`, with nothing from pkc-js behind it. ### 3. Apply it before creating the publication Filter the text before it reaches `pkc.createComment()`, so the transformation happens before any signature exists. Nothing needs to re-sign, and there is no window where an unfiltered draft is signed. ```js const configs = getWordfilterConfigs(community); // One call per field, carrying the rules of every challenge that covers that field. Do not apply the // challenges one at a time: a later challenge's replacement can reintroduce an earlier challenge's // src, and the earlier challenge would then reject the publication. The loop inside applyWordfilters // only sees that interaction when both rule sets are in the same call. const rulesFor = (fieldName) => configs.filter(({ fieldNames }) => fieldNames.includes(fieldName)).flatMap(({ rules }) => rules); if (content) content = applyWordfilters(content, rulesFor("comment.content")); if (title) title = applyWordfilters(title, rulesFor("comment.title")); if (displayName) displayName = applyWordfilters(displayName, rulesFor("comment.author.displayName")); const comment = await pkc.createComment({ content, title, author: { displayName }, communityAddress, signer }); await comment.publish(); ``` The challenge runs on every publication type, not only new comments. The [default field list](#default-fields) also names the text of a comment edit and the display name on a vote, so the same `rulesFor` call applies before each of these, with the path prefixed by the publication type: ```js // Comment edit: content and reason are top-level on the edit, displayName is under author. const edit = await pkc.createCommentEdit({ commentCid, content: applyWordfilters(newContent, rulesFor("commentEdit.content")), reason: applyWordfilters(reason, rulesFor("commentEdit.reason")), author: { displayName: applyWordfilters(displayName, rulesFor("commentEdit.author.displayName")) }, communityAddress, signer }); // Vote: the only text on it is the display name. const vote = await pkc.createVote({ commentCid, vote: 1, author: { displayName: applyWordfilters(displayName, rulesFor("vote.author.displayName")) }, communityAddress, signer }); ``` Those paths are the defaults, so they are the minimum. If a community publishes other paths in `wordfilter/v1/fieldNames`, run the same call on whatever your client puts at those paths; the community checks exactly the configured paths and nothing else, so an unfiltered custom field is a rejection just like an unfiltered display name. The paths a community is most likely to add are moderator and owner text, which `rulesFor` returns empty for unless the community names them: ```js // Comment moderation: the moderator's reason lives under the publication's commentModeration property, // hence the doubled segment. const moderation = await pkc.createCommentModeration({ commentCid, commentModeration: { removed: true, reason: applyWordfilters(reason, rulesFor("commentModeration.commentModeration.reason")) }, communityAddress, signer }); // Community edit: title and description live under the publication's communityEdit property. const communityEdit = await pkc.createCommunityEdit({ communityEdit: { title: applyWordfilters(title, rulesFor("communityEdit.communityEdit.title")), description: applyWordfilters(description, rulesFor("communityEdit.communityEdit.description")) }, communityAddress, signer }); ``` The `wordfilter/v1/fieldNames` entries are dot-notation paths, the same convention `publication-match` uses for `propertyName`, resolved against the challenge request's publication map rather than the publication itself. So the first segment names the publication type, and a path is simply absent, and passes, on a request carrying a different type: `commentEdit.content` never sees a new comment, and `comment.content` never sees an edit. Two challenges on the same community whose rules undo each other, `foo → bar` in one and `bar → foo` in another, cannot be caught by either challenge's `validateChallengeSettings`, which sees only its own settings. The merged call then either throws `did not stabilise` or settles on text that one of the challenges still rejects, and the community returns that challenge's `error`. Both are the right outcome: no text containing either word can satisfy both challenges, so the board is misconfigured and the owner has to fix it. Handle it the same way as any other rejection, in step 5. ### 4. Show the user what happened Nothing in the protocol requires it, but the author is signing text they did not type. Showing the filtered result before publishing, or a note afterwards, avoids the surprise. This is a UX decision, not a correctness one. ### 5. Handle rejection Your copy of the community record can be older than the community's current settings. An owner who adds a rule starts enforcing it immediately, while your client keeps publishing against the rules it last saw. When that happens the publication is rejected with the challenge's `error` message. Refresh the community, re-apply, and let the author retry. **Do not re-sign automatically:** that would silently publish text the author never reviewed, which is the exact failure this design exists to prevent. ## Requirements - pkc-js `>=0.0.85`, for `publicOptions` ([pkc-js#282](https://github.com/pkcprotocol/pkc-js/issues/282)) and `validateChallengeSettings` ([pkc-js#283](https://github.com/pkcprotocol/pkc-js/issues/283)) - Node.js `>=22` - ESM-only environment ## Install ### With bitsocial-cli ```bash bitsocial challenge install @bitsocial/wordfilter-challenge ``` ```bash bitsocial community edit your-community.bso \ '--settings.challenges[0].name' @bitsocial/wordfilter-challenge \ '--settings.challenges[0].options.wordfilter/v1/rules' '[{"src":"cloud","dst":"butt"}]' \ '--settings.challenges[0].publicOptions[0]' wordfilter/v1/rules ``` ### With pkc-js over RPC Install the challenge on the RPC server, then set it on your community by name. Nothing has to be installed on the client side: ```bash bitsocial challenge install @bitsocial/wordfilter-challenge ``` ### With pkc-js (TypeScript) Running your own node locally, without RPC: ```bash npm install @bitsocial/wordfilter-challenge ``` ```ts import PKC from "@pkcprotocol/pkc-js"; import { wordfilterChallenge } from "@bitsocial/wordfilter-challenge"; PKC.challenges["@bitsocial/wordfilter-challenge"] = wordfilterChallenge; ``` ## Configuration For community owners, set in `settings.challenges`: ```js await community.edit({ settings: { challenges: [ { name: "@bitsocial/wordfilter-challenge", options: { "wordfilter/v1/rules": JSON.stringify([ { src: "cloud", dst: "butt" }, { src: "millennials", dst: "snake people" }, { src: "spamword", dst: "" } ]), "wordfilter/v1/fieldNames": JSON.stringify([ "comment.content", "comment.title", "comment.author.displayName", "commentEdit.content", "commentEdit.reason", "commentEdit.author.displayName", "vote.author.displayName" ]), error: "This board replaces certain words. Please repost with the replacements applied." }, publicOptions: ["wordfilter/v1/rules", "wordfilter/v1/fieldNames", "error"] } ] } }); ``` `publicOptions` is **required**, not decorative. Options are private by default in pkc-js, and a client that cannot read the rules cannot satisfy them. The challenge's `validateChallengeSettings` hook rejects the edit if `wordfilter/v1/rules` is missing from `publicOptions`, so the failure surfaces when you save rather than when every author starts getting rejected. ### Options | Option | Required | Must be in `publicOptions` | Description | |---|---|---|---| | `wordfilter/v1/rules` | yes | yes | JSON array of `{ src, dst }`, applied in array order, cascading | | `wordfilter/v1/fieldNames` | no | when set | JSON array of dot-notation paths, each starting with the publication type. Defaults to the list under [Default fields](#default-fields) | | `error` | no | owner's call | Message shown to the author when a publication is rejected | An option has to be public when a client cannot satisfy the challenge without reading it. That covers `wordfilter/v1/rules` always, and `wordfilter/v1/fieldNames` whenever the owner sets it: a client filtering the default fields cannot satisfy a community checking a different set, and nothing in the published record would explain the rejections. Those two are also exactly the options the contract namespaces, for the same reason. `error` is returned in the rejection itself, so publishing it is transparency rather than a requirement, and it keeps its plain name. ### Validation `validateChallengeSettings` rejects at edit time: - `wordfilter/v1/rules` missing from `publicOptions`, or unparseable JSON, or not an array of `{ src, dst }` strings - an empty `src` - `src === dst` - the same `src` in more than one rule - any `dst` containing any `src` - more than 64 rules, or a `src` or `dst` longer than 128 characters - `wordfilter/v1/fieldNames`, when set, unparseable, not an array of non-empty strings, containing a path whose first segment is not a publication type (`comment`, `vote`, `commentEdit`, `commentModeration`, `communityEdit`), or missing from `publicOptions` The `src === dst`, duplicate `src` and `dst`-contains-`src` checks all compare case-insensitively, because matching is case-insensitive: `{src: "LOL", dst: "lol"}` replaces nothing just as surely as `{src: "lol", dst: "lol"}` does. The `dst` containing `src` rule is what guarantees the client's loop terminates. Without it a rule like `lol` becomes `lolol` produces output that always contains a filtered word, making every post permanently unpublishable. ## Migrating from 0.1.x and 0.2.0 0.2.0 already uses the `wordfilter/v1` option keys; only the paths inside `wordfilter/v1/fieldNames` change for it, see below. 0.1.x used bare `wordfilters` and `fieldNames` option names, which made the package rather than the contract the thing clients keyed off. Rename both, in `options` and in `publicOptions`: | 0.1.x | Now | |---|---| | `wordfilters` | `wordfilter/v1/rules` | | `fieldNames` | `wordfilter/v1/fieldNames` | | `error` | `error`, unchanged | The paths inside `fieldNames` change too, for 0.2.0 as much as 0.1.x. Both resolved them against the publication, so `content` meant "content on whatever was published". `wordfilter/v1/fieldNames` now resolves them against the challenge request's publication map, so every path starts with the publication type, and a bare `content` is rejected rather than silently matching nothing: | 0.1.x / 0.2.0 path | Now | |---|---| | `content` | `comment.content`, and `commentEdit.content` if edits should be filtered too | | `title` | `comment.title` | | `author.displayName` | one entry per publication type: `comment.author.displayName`, `commentEdit.author.displayName`, `vote.author.displayName` | If 0.1.x or 0.2.0 ran with the default field list, dropping `fieldNames` entirely and taking the new defaults is the closest equivalent. A 0.2.0 community that set `wordfilter/v1/fieldNames` with bare paths fails loudly at the next start, `validateChallengeSettings` naming the offending path, until the paths are prefixed. There is no fallback to the old names, on purpose. A wordfilter that quietly stops filtering is the exact failure this package exists to prevent, so an unmigrated community fails loudly instead: pkc-js rejects the now-undeclared option at community start with `ERR_CHALLENGE_OPTION_NOT_DECLARED_IN_OPTION_INPUTS`, naming the offending option, and `validateChallengeSettings` rejects the edit if you try to save it. Clients written against 0.1.x need the same rename in whatever they copied from this README. ## Matching semantics - **Literal, never regex.** vichan supports arbitrary PCRE, but only because its config is a server-local file the operator wrote. Here the rules ship inside a signed record that every browser client downloads and executes, so patterns are literal strings. - **Case-insensitive**, following vichan's `str_ireplace`. `cloud`, `Cloud`, and `CLOUD` are all replaced. Casing is not preserved: all three become `butt`. - **Cascading in array order**, and rules from multiple wordfilter challenges compose in challenge order. - **Absent fields pass cleanly.** A vote has no `content`, and that is not a failure. (`publication-match` treats a missing property as a failure, which would reject every vote. Do not copy that.) ## What this does not catch Deliberate evasion. `c l o u d`, `clοud` (with a Greek omicron), and zero-width-joined variants pass straight through. That is a hard limit, not an oversight. jschan has a `strictFiltering` mode that matches against NFD-stripped, zero-width-stripped and alphanumeric-only permutations of the post, and it is necessarily **detect-only**: normalisation is not invertible, so once you have matched a normalised form you no longer know where in the original text to splice a replacement. Evasion-resistant matching and replacement are mutually exclusive. For blocking evasive spam and slurs outright, use pkc-js's built-in `publication-match` challenge. Rejecting a publication does not require knowing where the match was. Note that `exclude` can skip a challenge for moderators or high-karma authors, which applies to this package too. ## Prior art - jschan, which powers soyjak.party: [`checkfilters.js`](https://gitgud.io/fatchan/jschan/-/blob/master/lib/post/checkfilters.js), [`filteractions.js`](https://gitgud.io/fatchan/jschan/-/blob/master/lib/post/filteractions.js), [`getfilterstrings.js`](https://gitgud.io/fatchan/jschan/-/blob/master/lib/post/getfilterstrings.js) - vichan: [`wordfilters()`](https://github.com/vichan-devel/vichan/blob/master/inc/functions.php#L1798) Both rewrite server-side at post time and keep no copy of the original. ## Development ```bash npm install npm run typecheck npm test npm run build ``` | Path | What it is | |---|---| | `src/wordfilter-challenge.ts` | The `ChallengeFileFactory`: `optionInputs`, `getChallenge`, `validateChallengeSettings` | | `src/apply-wordfilters.ts` | The client-side replacement loop and the `wordfilter/v1` option keys. Imports nothing, so it bundles for a browser | | `src/types.ts` | Re-exports of the pkc-js challenge types | ## License GPL-3.0-or-later, the same as pkc-js. ### @bitsocial/captcha-canvas-challenge Repository: bitsocialnet/captcha-canvas-challenge Source: https://github.com/bitsocialnet/captcha-canvas-challenge#readme Description: Standalone captcha canvas challenge extracted from pkc js, packaged with independent dependencies. # @bitsocial/captcha-canvas-challenge Standalone captcha canvas challenge extracted from `pkc-js`, packaged with independent dependencies. ## Security Warning This captcha can be brute-forced easily and should not be used to protect a real community. ## Status - `@pkcprotocol/pkc-js` is a dev dependency used only for TypeScript types. It is not required at runtime. ## Requirements - Node.js `>=22` - ESM-only environment ## Using captcha-canvas in your community Community owners add the captcha-canvas challenge to their community settings. When enabled, every publication (post, reply, vote) requires the author to solve a captcha. The challenge is published as [`@bitsocial/captcha-canvas-challenge`](https://www.npmjs.com/package/@bitsocial/captcha-canvas-challenge) on npm. The challenge name is the npm package name, `@bitsocial/captcha-canvas-challenge`, on every install path — that's the key in `PKC.challenges` and the value passed as `community.settings.challenges[].name`. `bitsocial challenge install` registers it this way automatically; manual registration should match. ### With pkc-js over RPC If your RPC server is already running, first install the challenge on the server: ```bash bitsocial challenge install @bitsocial/captcha-canvas-challenge ``` Then from your RPC client, connect and set the challenge on your community by name — no npm install or challenge registration needed on the client side: ```ts import PKC from "@pkcprotocol/pkc-js"; const pkc = await PKC({ pkcRpcClientsOptions: ["ws://localhost:9138"] }); const community = await pkc.createCommunity({ address: "your-community-address.bso" }); await community.edit({ settings: { challenges: [ { name: "@bitsocial/captcha-canvas-challenge", options: { characters: "6", width: "300", height: "100", colors: "#32cf7e" } } ] } }); ``` ### With pkc-js (TypeScript) Install the challenge package: ```bash npm install @bitsocial/captcha-canvas-challenge ``` Register the challenge and configure your community: ```typescript import PKC from '@pkcprotocol/pkc-js' import { captchaCanvasChallenge } from '@bitsocial/captcha-canvas-challenge' // Register the challenge so it can be referenced by name PKC.challenges['@bitsocial/captcha-canvas-challenge'] = captchaCanvasChallenge const pkc = await PKC({ /* your pkc options */ }) const community = await pkc.createCommunity({ address: 'your-community.bso' }) await community.edit({ settings: { challenges: [{ name: '@bitsocial/captcha-canvas-challenge', options: { characters: '6', width: '300', height: '100', colors: '#32cf7e', } }] } }) ``` #### Challenge options All option values must be strings (pkc-js challenge convention). | Option | Default | Description | |--------|---------|-------------| | `characters` | `"6"` | Amount of characters of the captcha | | `height` | `"100"` | Height of the captcha in pixels | | `width` | `"300"` | Width of the captcha in pixels | | `colors` | `"#32cf7e"` | Colors of the captcha text as hex comma separated values | ### With bitsocial-cli Install the challenge package: ```bash bitsocial challenge install @bitsocial/captcha-canvas-challenge ``` Edit your community to use the challenge: ```bash bitsocial community edit your-community.bso \ '--settings.challenges[0].name' @bitsocial/captcha-canvas-challenge \ '--settings.challenges[0].options.characters' '6' \ '--settings.challenges[0].options.width' '300' \ '--settings.challenges[0].options.height' '100' \ '--settings.challenges[0].options.colors' '#32cf7e' ``` See the [bitsocial-cli documentation](https://github.com/bitsocialnet/bitsocial-cli) for full CLI reference. ## Scripts ```bash npm run typecheck npm run build npm test ``` ### @bitsocial/voucher-challenge Repository: bitsocialnet/voucher-challenge Source: https://github.com/bitsocialnet/voucher-challenge#readme Description: Standalone voucher challenge extracted from plebbit js, packaged with independent dependencies. # @bitsocial/voucher-challenge Standalone voucher challenge extracted from `plebbit-js`, packaged with independent dependencies. ## How it works Community owners configure a list of voucher codes on their community. When a user publishes for the first time, they are prompted to enter a voucher code. Once redeemed, the voucher becomes permanently bound to the user's `author.address` — that author can continue using the same voucher for future publications (posts, replies, votes), but no other author can claim it. This prevents voucher sharing across different users. ## Requirements - Node.js `>=22` - ESM-only environment ## Install ### With bitsocial-cli ```bash bitsocial challenge install @bitsocial/voucher-challenge ``` Edit your community to use the challenge: ```bash bitsocial community edit your-community.bso \ '--settings.challenges[0].name' @bitsocial/voucher-challenge \ '--settings.challenges[0].options.vouchers' 'VOUCHER1,VOUCHER2,VOUCHER3' ``` ### With pkc-js over RPC If your RPC server is already running, first install the challenge on the server: ```bash bitsocial challenge install @bitsocial/voucher-challenge ``` Then from your RPC client, connect and set the challenge on your community by name — no npm install or challenge registration needed on the client side: ```ts import PKC from "@pkcprotocol/pkc-js"; const pkc = await PKC({ pkcRpcClientsOptions: ["ws://localhost:9138"] }); const community = await pkc.createCommunity({ address: "your-community-address.bso" }); await community.edit({ settings: { challenges: [ { name: "@bitsocial/voucher-challenge", options: { vouchers: "VOUCHER1,VOUCHER2,VOUCHER3" } } ] } }); ``` ### With pkc-js (TypeScript) If you are running your own node locally without connecting over RPC, you can install via npm and register the challenge manually: ```bash npm install @bitsocial/voucher-challenge ``` ```ts import PKC from "@pkcprotocol/pkc-js"; import { voucherChallenge } from "@bitsocial/voucher-challenge"; PKC.challenges["@bitsocial/voucher-challenge"] = voucherChallenge; ``` Then set the challenge on your community: ```ts await community.edit({ settings: { challenges: [ { name: "@bitsocial/voucher-challenge", options: { vouchers: "VOUCHER1,VOUCHER2,VOUCHER3" } } ] } }); ``` ## Challenge Options All option values must be strings. | Option | Default | Description | |--------|---------|-------------| | `question` | `"What is your voucher code?"` | The interactive prompt the user is asked to type an answer to | | `vouchers` | *(required)* | Comma-separated list of voucher codes | | `description` | — | Informational text shown in the UI explaining what the challenge is about | | `invalidVoucherError` | Default message | Error shown for invalid voucher codes | | `alreadyRedeemedError` | Default message | Error shown when a voucher is already redeemed by another author | ## Scripts ```bash npm run typecheck npm run build npm test ``` ### @bitsocial/evm-contract-challenge Repository: bitsocialnet/evm-contract-challenge Source: https://github.com/bitsocialnet/evm-contract-challenge#readme Description: An automatic challenge for @pkcprotocol/pkc js communities that verifies an author's EVM wallet address meets a condition from a smart contract call. # @bitsocial/evm-contract-challenge An automatic challenge for `@pkcprotocol/pkc-js` communities that verifies an author's EVM wallet address meets a condition from a smart contract call. ## How it works When an author publishes to a community with this challenge enabled, the community node calls a read-only smart contract method with the author's wallet address as the argument and compares the return value against a configured condition (e.g. `>1000`). The challenge tries three sources for the wallet address: 1. **Wallet address** — the `author.wallets[chainTicker]` address, verified via EIP-191 signature 2. **ENS/BSO domain** — if the author's address is a `.eth` or `.bso` domain, it resolves to an on-chain address 3. **NFT avatar** — the current owner of the author's avatar NFT If any source produces a wallet that passes the contract call condition, the challenge succeeds. No user interaction is required. ## Requirements - Node.js `>=22` - ESM-only environment ## Install ### With bitsocial-cli ```bash bitsocial challenge install @bitsocial/evm-contract-challenge ``` Edit your community to use the challenge: ```bash bitsocial community edit your-community.bso \ '--settings.challenges[0].name' @bitsocial/evm-contract-challenge \ '--settings.challenges[0].options.chainTicker' eth \ '--settings.challenges[0].options.address' '0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f' \ '--settings.challenges[0].options.abi' '{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}' \ '--settings.challenges[0].options.condition' '>10000000000000000000' \ '--settings.challenges[0].options.error' 'You need at least 10 Bitsocial tokens to post.' ``` ### With pkc-js over RPC If your RPC server is already running, first install the challenge on the server: ```bash bitsocial challenge install @bitsocial/evm-contract-challenge ``` Then from your RPC client, connect and set the challenge on your community by name — no npm install or challenge registration needed on the client side: ```ts import PKC from "@pkcprotocol/pkc-js"; const pkc = await PKC({ pkcRpcClientsOptions: ["ws://localhost:9138"] }); const community = await pkc.createCommunity({ address: "your-community-address.bso" }); await community.edit({ settings: { challenges: [ { name: "@bitsocial/evm-contract-challenge", options: { chainTicker: "eth", address: "0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f", abi: '{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}', condition: ">10000000000000000000", error: "You need at least 10 Bitsocial tokens to post." } } ] } }); ``` ### With pkc-js (TypeScript) If you are running your own node locally without connecting over RPC, you can install via npm and register the challenge manually: ```bash npm install @bitsocial/evm-contract-challenge ``` ```ts import PKC from "@pkcprotocol/pkc-js"; import { evmContractChallenge } from "@bitsocial/evm-contract-challenge"; PKC.challenges["@bitsocial/evm-contract-challenge"] = evmContractChallenge; ``` Then set the challenge on your community: ```ts await community.edit({ settings: { challenges: [ { name: "@bitsocial/evm-contract-challenge", options: { chainTicker: "eth", address: "0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f", abi: '{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}', condition: ">10000000000000000000", error: "You need at least 10 Bitsocial tokens to post." } } ] } }); ``` ## Example Challenges Each example uses a read-only contract function that takes a single `address` argument. The `condition` compares against the raw return value including decimal places (e.g. 10 USDC with 6 decimals = `10000000` raw). ### Common ABIs **`balanceOf`** — standard ERC-20 / ERC-721 token balance: ```json {"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"} ``` **`getScore`** — Gitcoin Passport score (returns `uint256` with 4 decimals): ```json {"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"getScore","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"} ``` ### Examples | Description | `chainTicker` | `address` | ABI | `condition` | |---|---|---|---|---| | At least 10 Bitsocial (BSO) tokens | `eth` | `0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f` | [`balanceOf`](#balanceof-abi) | `>10000000000000000000` | | Minimum 10 USDC | `eth` | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | [`balanceOf`](#balanceof-abi) | `>10000000` | | Any WETH balance | `eth` | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | [`balanceOf`](#balanceof-abi) | `>0` | | Gitcoin Passport score above 20 (proof of personhood) | `op` | `0xd6c51bB9E23bD7f1fEa22A3F2f85E3BFC8338Cb0` | [`getScore`](#getscore-abi) | `>200000` | | At least 10 MATIC on Polygon | `matic` | `0x0000000000000000000000000000000000001010` | [`balanceOf`](#balanceof-abi) | `>10000000000000000000` | | Any stETH balance (Lido staked ETH) | `eth` | `0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84` | [`balanceOf`](#balanceof-abi) | `>0` | > Setting `rpcUrls` is recommended on every chain, including Ethereum mainnet. See [Challenge Options](#challenge-options). ## Challenge Options All option values must be strings. | Option | Required | Default | Description | |--------|----------|---------|-------------| | `chainTicker` | yes | `"eth"` | The chain ticker (e.g. `eth`, `matic`) | | `rpcUrls` | see below | — | Comma-separated JSON-RPC URLs for the chain | | `address` | yes | — | The contract address to call | | `abi` | yes | — | The ABI of the contract method as a JSON object (not an array) | | `condition` | yes | — | Condition the return value must pass (`=`, `>`, or `<` followed by a value, e.g. `>1000`) | | `error` | no | `"Contract call response doesn't pass condition."` | Custom error message shown when the condition fails | ### When `rpcUrls` is required `rpcUrls` is **required unless `chainTicker` is one of the tickers below**, which have a built-in RPC endpoint: `eth`, `matic` / `pol`, `op`, `arb`, `base`, `avax`, `bnb` / `bsc`, `gno` / `xdai`, `celo`, `ftm`, `linea`, `scroll`, `zksync`, `blast` Setting it is still **recommended even for those**: the built-in endpoints are shared public RPCs and will rate-limit a busy community, which shows up as authors being rejected at random rather than as an obvious failure. An unsupported ticker with no `rpcUrls` is rejected when you save the challenge settings, rather than failing later for every author who tries to publish. ## Publishing Options `community.settings.challenges[i].options` is private. Nothing in it is published unless you name the option in `publicOptions`, which copies it into the public `community.challenges[i].publicOptions` record. **Recommended: publish everything except `rpcUrls`.** ```ts await community.edit({ settings: { challenges: [ { name: "@bitsocial/evm-contract-challenge", options: { /* ... */ }, publicOptions: ["chainTicker", "address", "abi", "condition", "error"] } ] } }); ``` | Option | Publish? | Why | |--------|----------|-----| | `chainTicker` | recommended | Already public in effect — the challenge's `type` is `chain/` | | `address` | recommended | A contract address is public on-chain data; hiding it buys no security | | `abi` | recommended | The signature of a public method on that contract | | `condition` | recommended | The threshold that makes the other three actionable | | `error` | recommended | Authors see this string anyway when they fail | | `rpcUrls` | **never — refused** | RPC URLs commonly embed a provider API key | Published together, `chainTicker` + `address` + `abi` + `condition` let a client tell an author exactly what they are missing *before* they publish and burn a challenge attempt. Published partially, none of that works, so treat the four as one unit. Keeping them private is a legitimate choice — you may not want the exact threshold known — and this package will not stop you. `rpcUrls` is the one exception: naming it in `publicOptions` is refused outright, because publishing a URL like `https://eth-mainnet.g.alchemy.com/v2/` leaks a paid credential to everyone, permanently, and no client ever uses these endpoints. Only the community node does. ## Settings Validation The challenge validates its own settings whenever you create a community, edit one, or start one, via pkc-js's `validateChallengeSettings` hook. A bad setting is rejected as a failed edit instead of silently rejecting every author at publish time. Rejected settings: - `publicOptions` naming `rpcUrls` - `rpcUrls` entries that are not valid URLs, or that do not use `http:` / `https:` - `rpcUrls` omitted for a `chainTicker` with no built-in RPC - `address` that is not a well-formed EVM address - `abi` that is not valid JSON, or not a single function entry taking one `address` and returning at least one value - `condition` with no supported operator (`=`, `>`, `<`), no value after the operator, or an ordering comparison against a non-numeric value (e.g. `>abc`, which would otherwise compare as text) pkc-js additionally rejects an option name that no option above declares, and any required option left unset, before this challenge's own validation runs. ## Multiple RPC URLs You can provide multiple RPC endpoints as a comma-separated string: ``` https://eth.llamarpc.com,https://rpc.ankr.com/eth,https://eth.drpc.org ``` When multiple URLs are provided, viem's [`fallback`](https://viem.sh/docs/clients/transports/fallback) transport is used with automatic ranking enabled (`rank: true`). This means: - Requests are sent to the highest-ranked RPC endpoint - If a request fails, it automatically falls back to the next endpoint - viem periodically pings all endpoints in the background and reorders them by latency and stability - A single URL works the same as before (no fallback overhead) - If `rpcUrls` is omitted, the chain's built-in RPC is used, which requires a supported `chainTicker` (see [When `rpcUrls` is required](#when-rpcurls-is-required)) This improves reliability — if one RPC provider goes down, the challenge automatically uses the next available endpoint. ## Scripts ```bash npm run typecheck npm run build npm test ``` ### @bitsocial/flags-challenge Repository: bitsocialnet/flags-challenge Source: https://github.com/bitsocialnet/flags-challenge#readme Description: Verified flag issuer challenge for Bitsocial communities and clients. # @bitsocial/flags-challenge Verified flag issuer challenge for Bitsocial communities and clients. This package runs on a Bitsocial community owner node as a challenge. It is not specific to 5chan: any Bitsocial client can use the same challenge pattern with its own issuer service, namespace, and flag profile. The first bundled profile is the 5chan profile. Country flags are issued by `flags.5chan.app`; `/pol/` memeflags and `/mlp/` pony flags are validated as board-choice flairs without an issuer iframe. For issuer-verified country flags, the challenge writes two pieces of data: - immutable comment data under a client namespace such as `comment["5chan"]`; - a compatibility mirror under `commentUpdate.author.community.flairs`, so clients that already render author flairs can show the same flag. For `/pol/` memeflags and `/mlp/` pony flags, clients should publish the selected flag as normal comment flair data. The challenge validates that the requested family is allowed and that the code is known, then accepts the publication without contacting the issuer service. The namespace is configurable. For example, a future Seedit profile could write to `comment["seedit"]` while using its own issuer and flag metadata. ## Installation Run this on the Bitsocial node that owns the community: ```bash bitsocial challenge install @bitsocial/flags-challenge ``` ## Configuration Add the challenge to the community's `settings.challenges`: ```js [{ name: "@bitsocial/flags-challenge" }]; ``` Default options use the first bundled profile and target the 5chan issuer: | Option | Default | Behavior | | -------------- | -------------------------------- | ----------------------------------------------------- | | `serviceUrl` | `https://flags.5chan.app/api/v1` | Flag issuer service endpoint | | `issuer` | `flags.5chan.app` | Expected issuer name in signed assertions | | `namespace` | `5chan` | Top-level comment object key for immutable data | | `profile` | `5chan` | Flag profile to validate; this release supports 5chan | | `allowedFlags` | `country,pol,pony` | Comma-separated flag families accepted by the board | | `emitFlair` | `true` | Mirrors the verified flag to author community flairs | | `error` | `Flag verification failed.` | Error prefix shown if verification fails | Boards can restrict flag families: ```js [ { name: "@bitsocial/flags-challenge", options: { allowedFlags: "country,pol", }, }, ]; ``` For `/mlp/`, use: ```js [ { name: "@bitsocial/flags-challenge", options: { allowedFlags: "pony", }, }, ]; ``` ## 5chan Flag Requests Clients request a flag by publishing a challenge-readable flag value on the comment, usually through `flair`: ```js { flair: { type: "country", code: "auto", text: "flag:country:auto" } } ``` Supported 5chan request strings: - `flag:country:auto` - `flag:pol:AC` - `flag:pony:AJ` Country flags use `auto` because the issuer service must derive the country from the challenge iframe request IP. The challenge does not trust a client-provided country code as proof of location. `/pol/` memeflags and `/mlp/` pony flags are free user choices, so they do not require the iframe flow. ## Verified Comment Shape For a country flag, the challenge returns a result like: ```js { success: true, comment: { "5chan": { country: "US", flag: { type: "country", code: "US", text: "flag:country:us", label: "US" }, issuer: "flags.5chan.app", issuedAt: 1770000000, signature: { publicKey: "flag-service-public-key", signature: "flag-service-signature", type: "ed25519" } } }, commentUpdate: { author: { community: { flairs: [{ text: "flag:country:us", type: "country", code: "US" }] } } } } ``` For `/pol/` memeflags and `/mlp/` pony flags, clients render the selected flag from the comment flair fields they publish. The challenge does not add a signed `comment["5chan"]` assertion for those board-choice flags. ## Issuer Service Contract The challenge creates a lazy iframe URL: ```text https://flags.5chan.app/api/v1/iframe//lazy#payload= ``` The payload includes: - the original challenge request; - `requestedFlag`; - `profile`, `namespace`, and `issuer`; - a community-signed CBOR payload signature. When the user completes the iframe flow, the challenge calls: ```text POST https://flags.5chan.app/api/v1/challenge/verify Content-Type: application/cbor Accept: application/json ``` The issuer should return: ```js { success: true, flag: { type: "country", code: "US", issuer: "flags.5chan.app", issuedAt: 1770000000, signature: { publicKey: "flag-service-public-key", signature: "flag-service-signature", type: "ed25519" } } } ``` For country flags, the service can also return `{ success: true, country: "US", issuer, issuedAt, signature }`. Issuer service deployment is intentionally outside this package. The challenge only defines the community-node integration and the issuer API contract. ## Development ```bash corepack yarn install corepack yarn type-check corepack yarn test corepack yarn test:coverage corepack yarn build ``` ## Publishing Create the GitHub release and changelog with release-it: ```bash corepack yarn release 0.1.0 ``` The command expects the current branch to have an upstream and needs a GitHub token that can create releases. It writes `CHANGELOG.md`, creates a `v0.1.0` tag, and opens the GitHub release. The first npm publish must create the package before trusted publishing can be configured: ```bash npm publish --access public ``` After the package exists, configure npm trusted publishing: - Publisher: GitHub Actions - Organization: `bitsocialnet` - Repository: `flags-challenge` - Workflow filename: `publish.yml` - Environment: leave blank Equivalent npm CLI command: ```bash npm trust github @bitsocial/flags-challenge --repo bitsocialnet/flags-challenge --file publish.yml ``` Future releases publish automatically when `package.json` version changes on `master`. The publish workflow skips versions that already exist on npm. ### bitsocial-cli: A Bitsocial Node with WebSocket and Command Line Interface Repository: bitsocialnet/bitsocial-cli Source: https://github.com/bitsocialnet/bitsocial-cli#readme Description: What is Bitsocial? # bitsocial-cli: A Bitsocial Node with WebSocket and Command Line Interface ## Table of contents - [What is Bitsocial?](#what-is-bitsocial) - [What is bitsocial-cli?](#what-is-bitsocial-cli) - [Install](#install) - [Docker](#docker) - [Usage](#usage) - [Commands](#commands) - [Contribution](#contribution) - [Feedback](#feedback) ## What is bitsocial-cli? `bitsocial-cli` is an interface to the backend of PKC protocol using [pkc-js](https://github.com/pkcprotocol/pkc-js). Users can run and manage their communities using it. It is written in Typescript and designed to receive commands via CLI and WebSocket. - Runs an IPFS and Bitsocial node - Command Line interface to manage Bitsocial communities - WebSocket RPC to access and control your communities and publications - Includes Web UIs like Seedit where you can browse the network and manage your community ## Install Requires Node.js 22 or later. We recommend using [nvm](https://github.com/nvm-sh/nvm) to install and manage Node.js versions. ```sh-session npm install -g @bitsocial/bitsocial-cli ``` To install a specific version: ```sh-session npm install -g @bitsocial/bitsocial-cli@0.19.39 ``` To update to the latest version: ```sh-session bitsocial update install ``` ### Build from source (optional) If you want to build from source directly: ``` git clone https://github.com/bitsocialnet/bitsocial-cli cd bitsocial-cli npm ci npm run build npx oclif manifest npm run ci:download-web-uis ./bin/run --help ``` After running the last command you should be able to run commands directly against `./bin/run`, for example `./bin/run daemon` ## Docker You can run bitsocial-cli as a Docker container. The container runs the daemon and exposes the RPC + web UI on port 9138, the Kubo IPFS API on port 50019, and the IPFS Gateway on port 6473. Once your container is running, you can use one of the bundled web UIs to browse the Bitsocial network and manage your communities -- no CLI commands needed. The web UIs provide a full-featured interface for creating communities, moderating, and browsing content entirely through your browser. All the Web UIs are interopable so you can post and read from whichever you like and you can see your own content on each client. If you're a power user, you can also run CLI commands against the running container with `docker exec`: ```sh-session docker exec bitsocial bitsocial community list ``` ### Data paths inside the container | Path | Description | |---|---| | `/data/bitsocial` | Bitsocial data directory | | `/data/bitsocial/communities` | Community SQLite databases | | `/data/bitsocial/.bitsocial-cli.ipfs` | Kubo IPFS repository | | `/logs/bitsocial` | Log files | The Docker volumes `bitsocial-data:/data` and `bitsocial-logs:/logs` are mapped to `/data` and `/logs` inside the container. The `bitsocial` subdirectory is created automatically by the application. ### Docker Compose (recommended) Copy the example compose file and start the node: ```sh-session cp docker-compose.example.yml docker-compose.yml docker compose up -d ``` View the startup logs to find your auth key URL: ```sh-session docker compose logs -f ``` The output will include lines like: ``` pkc rpc: listening on ws://localhost:9138/ (secret auth key for remote connections) WebUI (seedit - Similar to old reddit UI): http://:9138//seedit (secret auth key for remote connections) ``` Open the WebUI URL in your browser to start using Bitsocial. #### Viewing logs There are two ways to view logs from a Docker container: **Quick logs** — shows stdout output only (startup messages, errors): ```sh-session docker compose logs -f # Docker Compose docker logs -f bitsocial # Docker Run ``` **Full debug logs** — shows the complete daemon log including debug/trace output: ```sh-session docker exec bitsocial bitsocial logs -f ``` The `bitsocial logs` command supports several filtering flags: ```sh-session docker exec bitsocial bitsocial logs -f # follow (stream new lines) docker exec bitsocial bitsocial logs -n 100 # last 100 lines docker exec bitsocial bitsocial logs --since 1h # entries from the last hour docker exec bitsocial bitsocial logs --until 30m # entries up to 30 minutes ago ``` Debug and trace logs are written only to the log file, not to stdout, so `docker logs` will not show them. Use `bitsocial logs` inside the container for the full picture. #### Example docker-compose.yml ```yaml services: bitsocial: image: ghcr.io/bitsocialnet/bitsocial-cli:latest container_name: bitsocial restart: unless-stopped ports: - "9138:9138" # PKC RPC + Web UI - "50019:50019" # Kubo IPFS API - "6473:6473" # IPFS Gateway volumes: - bitsocial-data:/data - bitsocial-logs:/logs environment: - DEBUG=bitsocial*, pkc*, -pkc*trace # Set a fixed auth key (useful for bookmarking the web UI URL). # If left unset, a random key is generated on first start. # - PKC_RPC_AUTH_KEY=your-custom-auth-key-here # Override Kubo IPFS bind addresses / ports: # - KUBO_RPC_URL=http://0.0.0.0:50019/api/v0 # - IPFS_GATEWAY_URL=http://0.0.0.0:6473 volumes: bitsocial-data: bitsocial-logs: ``` ### Docker Run ```sh-session docker run -d \ --name bitsocial \ --restart unless-stopped \ -p 9138:9138 \ -p 50019:50019 \ -p 6473:6473 \ -v bitsocial-data:/data \ -v bitsocial-logs:/logs \ ghcr.io/bitsocialnet/bitsocial-cli:latest ``` With a custom auth key: ```sh-session docker run -d \ --name bitsocial \ --restart unless-stopped \ -p 9138:9138 \ -p 50019:50019 \ -p 6473:6473 \ -v bitsocial-data:/data \ -v bitsocial-logs:/logs \ -e PKC_RPC_AUTH_KEY=my-secret-key \ ghcr.io/bitsocialnet/bitsocial-cli:latest ``` ### Building the Docker image locally ```sh-session docker build -t bitsocial-cli . docker run -p 9138:9138 -p 50019:50019 -p 6473:6473 bitsocial-cli ``` ## Usage ### The data/config directory of Bitsocial This is the default directory where bitsocial-cli will keep its config, as well as data for local communities: - macOS: ~/Library/Application Support/bitsocial - Windows: %LOCALAPPDATA%\bitsocial - Linux: ~/.local/share/bitsocial ### The logs directory of Bitsocial bitsocial-cli will keep logs in this directory, with a cap of 10M per log file. - macOS: ~/Library/Logs/bitsocial - Windows: %LOCALAPPDATA%\bitsocial\Log - Linux: ~/.local/state/bitsocial ### Running Daemon In Bash (or powershell if you're on Windows), run `bitsocial daemon` to able to connect to the network. You need to have the `bitsocial daemon` terminal running to be able to execute other commands. ```sh-session $ bitsocial daemon IPFS API listening on: http://localhost:5001/api/v0 IPFS Gateway listening on: http://localhost:6473 pkc rpc: listening on ws://localhost:9138 (local connections only) pkc rpc: listening on ws://localhost:9138/ (secret auth key for remote connections) Bitsocial data path: /root/.local/share/bitsocial Communities in data path: [ 'anime-and-manga.bso' ] WebUI (5chan - Imageboard-style UI): http://localhost:9138//5chan (local connections only) WebUI (5chan - Imageboard-style UI): http://:9138//5chan (secret auth key for remote connections) WebUI (seedit - Similar to old reddit UI): http://localhost:9138//seedit (local connections only) WebUI (seedit - Similar to old reddit UI): http://:9138//seedit (secret auth key for remote connections) ``` Once `bitsocial daemon` is running, you can create and manage your communities through the web interfaces, either seedit or 5chan. All the interfaces are interoperable. If you're a power user and prefer CLI, then you can take a look at the commands below. If you need to view detailed protocol or IPFS logs for debugging, you can use `bitsocial logs`. For example, `bitsocial logs --tail 50` shows the last 50 lines, or `bitsocial logs --since 1h` shows logs from the past hour. #### Creating your first community ```sh-session $ bitsocial community create --title "Hello World!" --description "This is gonna be great" 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu ``` #### Listing all your communities ```sh-session $ bitsocial community list Address Started ──────────────────────────────────────────────────── ─────── 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu true business-and-finance.bso true censorship-watch.bso true health-nutrition-science.bso true movies-tv-anime.bso true anime-and-manga.bso true politically-incorrect.bso true reddit-screenshots.bso false videos-livestreams-podcasts.bso false ``` #### Adding a role moderator to your community ```sh-session $ bitsocial community edit mysub.bso '--roles["author-address.bso"].role' moderator ``` #### Adding a role owner to your community ```sh-session $ bitsocial community edit mysub.bso '--roles["author-address.bso"].role' owner ``` #### Adding a role admin to your community ```sh-session $ bitsocial community edit mysub.bso '--roles["author-address.bso"].role' admin ``` #### Removing a role ```sh-session $ bitsocial community edit mysub.bso '--roles["author-address.bso"]' null ``` ## Commands * [`bitsocial challenge add PACKAGE`](#bitsocial-challenge-add-package) * [`bitsocial challenge i PACKAGE`](#bitsocial-challenge-i-package) * [`bitsocial challenge install PACKAGE`](#bitsocial-challenge-install-package) * [`bitsocial challenge list`](#bitsocial-challenge-list) * [`bitsocial challenge ls`](#bitsocial-challenge-ls) * [`bitsocial challenge remove NAME`](#bitsocial-challenge-remove-name) * [`bitsocial challenge rm NAME`](#bitsocial-challenge-rm-name) * [`bitsocial challenge un NAME`](#bitsocial-challenge-un-name) * [`bitsocial challenge uninstall NAME`](#bitsocial-challenge-uninstall-name) * [`bitsocial community create`](#bitsocial-community-create) * [`bitsocial community delete ADDRESSES`](#bitsocial-community-delete-addresses) * [`bitsocial community edit ADDRESS`](#bitsocial-community-edit-address) * [`bitsocial community export [ADDRESS]`](#bitsocial-community-export-address) * [`bitsocial community get [ADDRESS]`](#bitsocial-community-get-address) * [`bitsocial community list`](#bitsocial-community-list) * [`bitsocial community start ADDRESSES`](#bitsocial-community-start-addresses) * [`bitsocial community stop ADDRESSES`](#bitsocial-community-stop-addresses) * [`bitsocial daemon`](#bitsocial-daemon) * [`bitsocial help [COMMAND]`](#bitsocial-help-command) * [`bitsocial logs`](#bitsocial-logs) * [`bitsocial update check`](#bitsocial-update-check) * [`bitsocial update install [VERSION]`](#bitsocial-update-install-version) * [`bitsocial update versions`](#bitsocial-update-versions) ## `bitsocial challenge add PACKAGE` Install a challenge package (npm package name, git URL, tarball URL, or local path) ``` USAGE $ bitsocial challenge add PACKAGE --pkcRpcUrl [--pkcOptions.dataPath ] ARGUMENTS PACKAGE Package specifier — anything npm can install (name, name@version, git URL, tarball URL, local path) FLAGS --pkcOptions.dataPath= Data path to install the challenge into --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Install a challenge package (npm package name, git URL, tarball URL, or local path) ALIASES $ bitsocial challenge i $ bitsocial challenge add EXAMPLES $ bitsocial challenge install @bitsocial/mintpass-challenge $ bitsocial challenge install @bitsocial/mintpass-challenge@1.0.0 $ bitsocial challenge install github:user/repo $ bitsocial challenge install https://example.com/my-challenge-1.0.0.tar.gz $ bitsocial challenge install ./my-local-challenge ``` ## `bitsocial challenge i PACKAGE` Install a challenge package (npm package name, git URL, tarball URL, or local path) ``` USAGE $ bitsocial challenge i PACKAGE --pkcRpcUrl [--pkcOptions.dataPath ] ARGUMENTS PACKAGE Package specifier — anything npm can install (name, name@version, git URL, tarball URL, local path) FLAGS --pkcOptions.dataPath= Data path to install the challenge into --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Install a challenge package (npm package name, git URL, tarball URL, or local path) ALIASES $ bitsocial challenge i $ bitsocial challenge add EXAMPLES $ bitsocial challenge install @bitsocial/mintpass-challenge $ bitsocial challenge install @bitsocial/mintpass-challenge@1.0.0 $ bitsocial challenge install github:user/repo $ bitsocial challenge install https://example.com/my-challenge-1.0.0.tar.gz $ bitsocial challenge install ./my-local-challenge ``` ## `bitsocial challenge install PACKAGE` Install a challenge package (npm package name, git URL, tarball URL, or local path) ``` USAGE $ bitsocial challenge install PACKAGE --pkcRpcUrl [--pkcOptions.dataPath ] ARGUMENTS PACKAGE Package specifier — anything npm can install (name, name@version, git URL, tarball URL, local path) FLAGS --pkcOptions.dataPath= Data path to install the challenge into --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Install a challenge package (npm package name, git URL, tarball URL, or local path) ALIASES $ bitsocial challenge i $ bitsocial challenge add EXAMPLES $ bitsocial challenge install @bitsocial/mintpass-challenge $ bitsocial challenge install @bitsocial/mintpass-challenge@1.0.0 $ bitsocial challenge install github:user/repo $ bitsocial challenge install https://example.com/my-challenge-1.0.0.tar.gz $ bitsocial challenge install ./my-local-challenge ``` _See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/challenge/install.ts)_ ## `bitsocial challenge list` List installed challenge packages ``` USAGE $ bitsocial challenge list [-q] [--pkcOptions.dataPath ] FLAGS -q, --quiet Only display challenge names --pkcOptions.dataPath= Data path where challenges are installed DESCRIPTION List installed challenge packages ALIASES $ bitsocial challenge ls EXAMPLES $ bitsocial challenge list $ bitsocial challenge list -q ``` _See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/challenge/list.ts)_ ## `bitsocial challenge ls` List installed challenge packages ``` USAGE $ bitsocial challenge ls [-q] [--pkcOptions.dataPath ] FLAGS -q, --quiet Only display challenge names --pkcOptions.dataPath= Data path where challenges are installed DESCRIPTION List installed challenge packages ALIASES $ bitsocial challenge ls EXAMPLES $ bitsocial challenge list $ bitsocial challenge list -q ``` ## `bitsocial challenge remove NAME` Remove an installed challenge package ``` USAGE $ bitsocial challenge remove NAME --pkcRpcUrl [--pkcOptions.dataPath ] ARGUMENTS NAME The challenge package name (e.g., my-challenge or @scope/my-challenge) FLAGS --pkcOptions.dataPath= Data path where challenges are installed --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Remove an installed challenge package ALIASES $ bitsocial challenge uninstall $ bitsocial challenge rm $ bitsocial challenge un EXAMPLES $ bitsocial challenge remove my-challenge $ bitsocial challenge remove @scope/my-challenge ``` _See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/challenge/remove.ts)_ ## `bitsocial challenge rm NAME` Remove an installed challenge package ``` USAGE $ bitsocial challenge rm NAME --pkcRpcUrl [--pkcOptions.dataPath ] ARGUMENTS NAME The challenge package name (e.g., my-challenge or @scope/my-challenge) FLAGS --pkcOptions.dataPath= Data path where challenges are installed --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Remove an installed challenge package ALIASES $ bitsocial challenge uninstall $ bitsocial challenge rm $ bitsocial challenge un EXAMPLES $ bitsocial challenge remove my-challenge $ bitsocial challenge remove @scope/my-challenge ``` ## `bitsocial challenge un NAME` Remove an installed challenge package ``` USAGE $ bitsocial challenge un NAME --pkcRpcUrl [--pkcOptions.dataPath ] ARGUMENTS NAME The challenge package name (e.g., my-challenge or @scope/my-challenge) FLAGS --pkcOptions.dataPath= Data path where challenges are installed --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Remove an installed challenge package ALIASES $ bitsocial challenge uninstall $ bitsocial challenge rm $ bitsocial challenge un EXAMPLES $ bitsocial challenge remove my-challenge $ bitsocial challenge remove @scope/my-challenge ``` ## `bitsocial challenge uninstall NAME` Remove an installed challenge package ``` USAGE $ bitsocial challenge uninstall NAME --pkcRpcUrl [--pkcOptions.dataPath ] ARGUMENTS NAME The challenge package name (e.g., my-challenge or @scope/my-challenge) FLAGS --pkcOptions.dataPath= Data path where challenges are installed --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Remove an installed challenge package ALIASES $ bitsocial challenge uninstall $ bitsocial challenge rm $ bitsocial challenge un EXAMPLES $ bitsocial challenge remove my-challenge $ bitsocial challenge remove @scope/my-challenge ``` ## `bitsocial community create` Create a community with specific properties. A newly created community will be started after creation and be able to receive publications. For a list of properties, visit https://github.com/pkcprotocol/pkc-js ``` USAGE $ bitsocial community create --pkcRpcUrl [--privateKeyPath ] [-f ] FLAGS -f, --jsonFile= Path to a JSON/JSONC file containing create options (supports comments) --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC --privateKeyPath= Private key (PEM) of the community signer that will be used to determine address (if address is not a domain). If it's not provided then PKC will generate a private key DESCRIPTION Create a community with specific properties. A newly created community will be started after creation and be able to receive publications. For a list of properties, visit https://github.com/pkcprotocol/pkc-js EXAMPLES Create a community with title 'Hello Plebs' and description 'Welcome' $ bitsocial community create --title 'Hello Plebs' --description 'Welcome' Create a community using options from a JSON/JSONC file $ bitsocial community create --jsonFile ./create-options.json ``` _See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/community/create.ts)_ ## `bitsocial community delete ADDRESSES` Delete a community permanently. ``` USAGE $ bitsocial community delete ADDRESSES... --pkcRpcUrl ARGUMENTS ADDRESSES... Addresses of communities to delete. Separated by space FLAGS --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Delete a community permanently. EXAMPLES $ bitsocial community delete plebbit.bso $ bitsocial community delete 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu ``` _See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/community/delete.ts)_ ## `bitsocial community edit ADDRESS` Edit a community's properties. For a list of properties, visit https://github.com/pkcprotocol/pkc-js ``` USAGE $ bitsocial community edit ADDRESS --pkcRpcUrl [-f ] ARGUMENTS ADDRESS Address of the community to edit. It could be the name domain, or a public key FLAGS -f, --jsonFile= Path to a JSON/JSONC file containing edit options (supports comments) --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Edit a community's properties. For a list of properties, visit https://github.com/pkcprotocol/pkc-js Merge behavior with CLI flags: - Objects are merged with the community's current state (new keys are added, existing keys are overwritten). - Arrays are extended: new values are prepended to the existing array. - Setting a value to null removes it (e.g. --roles['mod.bso'] null). Merge behavior with --jsonFile: - Objects are merged the same way as CLI flags. - Arrays are replaced entirely (RFC 7396 JSON Merge Patch semantics). - When both --jsonFile and CLI flags are provided, CLI flags take priority. For modifying complex settings like challenges, consider using a web UI instead: https://bitsocial.net/apps EXAMPLES Change the name of the community $ bitsocial community edit 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu --name newName.bso Add the author address 'esteban.bso' as an admin on the community $ bitsocial community edit mycommunity.bso '--roles["esteban.bso"].role' admin Add two challenges to the community. The first challenge will be a question and answer, and the second will be an image captcha $ bitsocial community edit mycommunity.bso --settings.challenges[0].name question \ --settings.challenges[0].options.question "what is the password?" --settings.challenges[0].options.answer \ thepassword --settings.challenges[1].name captcha-canvas-v3 Change the title and description $ bitsocial community edit mycommunity.bso --title "This is the new title" --description "This is the new \ description" Remove a role from a moderator/admin/owner $ bitsocial community edit bitsocial.bso --roles['rinse12.bso'] null Enable settings.fetchThumbnailUrls to fetch the thumbnail of url submitted by authors $ bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls disable settings.fetchThumbnailUrls $ bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls=false Edit a community using options from a JSON/JSONC file $ bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json ``` _See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/community/edit.ts)_ ## `bitsocial community export [ADDRESS]` Export a local community to a SQLite snapshot file. The export runs on the RPC server (daemon); once finished the snapshot is downloaded and its sha256 checksum is verified. Pass --includePrivateKey to produce a restorable backup that keeps the community's address. ``` USAGE $ bitsocial community export [ADDRESS] --pkcRpcUrl [--name ] [--publicKey ] [-o ] [--includePrivateKey] [--force] [-q] ARGUMENTS [ADDRESS] Address of the community to export FLAGS -o, --path= Destination file for the downloaded snapshot (default: /exports/
_.sqlite) -q, --quiet Suppress progress output; only print the path of the downloaded snapshot --force Overwrite the destination file if it already exists --includePrivateKey Ask the RPC server to include the community signer's private key in the export. Required for a restorable backup that keeps the same community address. The daemon may refuse (see `bitsocial daemon --no-allowPrivateKeyExport`) --name= Name of the community to export --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC --publicKey= Public key of the community to export DESCRIPTION Export a local community to a SQLite snapshot file. The export runs on the RPC server (daemon); once finished the snapshot is downloaded and its sha256 checksum is verified. Pass --includePrivateKey to produce a restorable backup that keeps the community's address. EXAMPLES $ bitsocial community export plebmusic.bso $ bitsocial community export plebmusic.bso --includePrivateKey -o ./backups/plebmusic.sqlite $ bitsocial community export --name my-community $ bitsocial community export --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu ``` _See code: [src/cli/commands/community/export.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/community/export.ts)_ ## `bitsocial community get [ADDRESS]` Fetch a local or remote community, and print its json in the terminal ``` USAGE $ bitsocial community get [ADDRESS] --pkcRpcUrl [--name ] [--publicKey ] ARGUMENTS [ADDRESS] Address of the community to fetch FLAGS --name= Name of the community to fetch --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC --publicKey= Public key of the community to fetch DESCRIPTION Fetch a local or remote community, and print its json in the terminal EXAMPLES $ bitsocial community get plebmusic.bso $ bitsocial community get 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu $ bitsocial community get --name my-community $ bitsocial community get --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu ``` _See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/community/get.ts)_ ## `bitsocial community list` List your communities ``` USAGE $ bitsocial community list --pkcRpcUrl [-q] FLAGS -q, --quiet Only display community addresses (much faster: skips the per-community 'started' lookup) --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION List your communities EXAMPLES $ bitsocial community list -q $ bitsocial community list ``` _See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/community/list.ts)_ ## `bitsocial community start ADDRESSES` Start a community ``` USAGE $ bitsocial community start ADDRESSES... --pkcRpcUrl [--concurrency ] ARGUMENTS ADDRESSES... Addresses of communities to start. Separated by space FLAGS --concurrency= [default: 5] Number of communities to start in parallel --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Start a community EXAMPLES $ bitsocial community start plebbit.bso $ bitsocial community start 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu Start all communities in your data path $ bitsocial community start $(bitsocial community list -q) Start communities sequentially (no concurrency) $ bitsocial community start $(bitsocial community list -q) --concurrency 1 ``` _See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/community/start.ts)_ ## `bitsocial community stop ADDRESSES` Stop a community. The community will not publish or receive any publications until it is started again. ``` USAGE $ bitsocial community stop ADDRESSES... --pkcRpcUrl ARGUMENTS ADDRESSES... Addresses of communities to stop. Separated by space FLAGS --pkcRpcUrl= (required) [default: ws://localhost:9138/] URL to PKC RPC DESCRIPTION Stop a community. The community will not publish or receive any publications until it is started again. EXAMPLES $ bitsocial community stop plebbit.bso $ bitsocial community stop Qmb99crTbSUfKXamXwZBe829Vf6w5w5TktPkb6WstC9RFW ``` _See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/community/stop.ts)_ ## `bitsocial daemon` Run a network-connected Bitsocial node. Once the daemon is running you can create and start your communities and receive publications from users. The daemon will also serve web ui on http that can be accessed through a browser on any machine. Within the web ui users are able to browse, create and manage their communities fully P2P. ``` USAGE $ bitsocial daemon --pkcRpcUrl --logPath [--chainProviderUrls ...] [--enableIpfsGc] [--ipfsGcIntervalMinutes ] [--allowPrivateKeyExport] FLAGS --[no-]allowPrivateKeyExport Allow RPC clients to request community exports that include the community signer's private key (`bitsocial community export --includePrivateKey`). Disable with --no-allowPrivateKeyExport when exposing the RPC to untrusted clients --chainProviderUrls=... [default: https://eth.drpc.org,https://ethereum.publicnode.com,https://ethereum-rpc.publicnode. com,https://rpc.mevblocker.io,https://1rpc.io/eth,https://eth-pokt.nodies.app] RPC URL(s) for .bso name resolution. Can be specified multiple times. --[no-]enableIpfsGc Periodically garbage-collect the IPFS repo over the kubo RPC API while the daemon is up. Only reclaims unpinned blocks — pinned data and MFS are never collected. Disable with --no-enableIpfsGc --ipfsGcIntervalMinutes= [default: 60] How often to garbage-collect the IPFS repo, in minutes --logPath= (required) [default: /home/runner/.local/state/bitsocial] Specify a directory which will be used to store logs --pkcRpcUrl= (required) [default: ws://localhost:9138/] Specify PKC RPC URL to listen on DESCRIPTION Run a network-connected Bitsocial node. Once the daemon is running you can create and start your communities and receive publications from users. The daemon will also serve web ui on http that can be accessed through a browser on any machine. Within the web ui users are able to browse, create and manage their communities fully P2P. Options can be passed to the RPC's instance through flag --pkcOptions.optionName. For a list of pkc options (https://github.com/pkcprotocol/pkc-js?tab=readme-ov-file#pkcoptions) If you need to modify ipfs config, you should head to {bitsocial-data-path}/.ipfs-bitsocial-cli/config and modify the config file EXAMPLES $ bitsocial daemon $ bitsocial daemon --pkcRpcUrl ws://localhost:53812 $ bitsocial daemon --pkcOptions.dataPath /tmp/bitsocial-datapath/ $ bitsocial daemon --pkcOptions.kuboRpcClientsOptions[0] https://remoteipfsnode.com $ bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY $ bitsocial daemon --no-allowPrivateKeyExport ``` _See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/daemon.ts)_ ## `bitsocial help [COMMAND]` Display help for bitsocial. ``` USAGE $ bitsocial help [COMMAND...] [-n] ARGUMENTS [COMMAND...] Command to show help for. FLAGS -n, --nested-commands Include all nested commands in the output. DESCRIPTION Display help for bitsocial. ``` ## `bitsocial logs` View the latest BitSocial daemon log file. By default dumps the full log and exits. Use --follow to stream new output in real-time (like tail -f). ``` USAGE $ bitsocial logs [-f] [-n ] [--since ] [--until ] [--logPath ] [--stdout | --stderr] FLAGS -f, --follow Follow log output in real-time (like tail -f) -n, --tail= [default: all] Number of log entries to show from the end. Use "all" to show everything. --logPath= Specify the directory containing log files --since= Show logs since timestamp (ISO 8601, e.g. 2026-01-02T13:23:37Z) or relative time (e.g. 30s, 42m, 2h, 1d) --stderr Show only stderr log entries (output of pkc-logger library) --stdout Show only stdout log entries --until= Show logs before timestamp (ISO 8601, e.g. 2026-01-02T13:23:37Z) or relative time (e.g. 30s, 42m, 2h, 1d) DESCRIPTION View the latest BitSocial daemon log file. By default dumps the full log and exits. Use --follow to stream new output in real-time (like tail -f). EXAMPLES $ bitsocial logs $ bitsocial logs -f $ bitsocial logs -n 50 $ bitsocial logs --since 5m $ bitsocial logs --since 2026-01-02T13:23:37Z --until 2026-01-02T14:00:00Z $ bitsocial logs --since 1h -f $ bitsocial logs --stdout $ bitsocial logs --stderr $ bitsocial logs --stdout -f ``` _See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/logs.ts)_ ## `bitsocial update check` Check if a newer version of bitsocial is available on npm ``` USAGE $ bitsocial update check DESCRIPTION Check if a newer version of bitsocial is available on npm EXAMPLES $ bitsocial update check ``` _See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/update/check.ts)_ ## `bitsocial update install [VERSION]` Install a specific version of bitsocial from npm ``` USAGE $ bitsocial update install [VERSION] [--force] [--restart-daemons] ARGUMENTS [VERSION] [default: latest] Version to install (e.g. "0.19.40" or "latest") FLAGS --force Reinstall even if already on the requested version --[no-]restart-daemons Stop all running daemons, update, and restart them with the same settings DESCRIPTION Install a specific version of bitsocial from npm EXAMPLES $ bitsocial update install $ bitsocial update install latest $ bitsocial update install 0.19.40 $ bitsocial update install --force $ bitsocial update install --no-restart-daemons ``` _See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/update/install.ts)_ ## `bitsocial update versions` List available bitsocial versions on npm ``` USAGE $ bitsocial update versions [--limit ] FLAGS --limit= [default: 20] Maximum number of versions to display DESCRIPTION List available bitsocial versions on npm EXAMPLES $ bitsocial update versions $ bitsocial update versions --limit 5 ``` _See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.96/src/cli/commands/update/versions.ts)_ ## Contribution We're always happy to receive pull requests. Few things to keep in mind: - This repo follows [Angular commit conventions](https://github.com/angular/angular/blob/main/CONTRIBUTING.md). Easiest way to follow these conventions is by using `npm run commit` instead of `git commit` - If you're adding a feature, make sure to add tests to your pull requests ## Feedback We would love your feedback on our community channels ### bitsocial-seeder Repository: bitsocialnet/bitsocial-seeder Source: https://github.com/bitsocialnet/bitsocial-seeder#readme Description: Seeds Bitsocial community first pages, post update CIDs, pubsub topic routing CIDs, and pubsub topics through a bitsocial daemon. # bitsocial-seeder Seeds Bitsocial community first pages, post-update CIDs, pubsub topic routing CIDs, and pubsub topics through a `bitsocial daemon`. It also seeds [directory vote contests](#seed-pubsub-votes-directory-contests), which needs no daemon at all. For community seeding it reuses an already-running Kubo and PKC RPC when one is available. If it cannot find a local daemon, it starts the bundled `@bitsocial/bitsocial-cli` daemon automatically and seeds through that node. A [votes-only seeder](#run-a-votes-only-seeder) never requires or starts one. ## Is this the only way to seed? No — and for most users it is not the recommended way. Bitsocial desktop apps such as the 5chan Electron app already seed automatically while they are running. If many users keep an app open, the network is well served without anyone running a dedicated seeder. **The apps are the load-bearing seeders of the protocol; this repo is supplemental.** `bitsocial-seeder` exists for operators who want to contribute consistent 24/7 seeding capacity from a VPS or a spare machine — for example, running closer to popular communities to lower fetch latency, or keeping data available during quiet periods when few app users are online. It is helpful but not required. This is also an **experimental repository**. Releases are cut frequently, internals change without warning between minor versions, and the project is treated as a place to try ideas that benefit the protocol but are not on its critical path. Please file issues if you hit anything; expect bumps. ## Setup The fastest path is Docker Compose. Recommended for unattended VPS seeders — it gives you a predictable service wrapper, simpler updates, and fewer local Node/native dependency surprises. **1. Clone and start the container:** ```sh git clone https://github.com/bitsocialnet/bitsocial-seeder.git cd bitsocial-seeder docker compose up -d ``` **2. Watch the logs to confirm it's seeding:** ```sh docker compose logs -f ``` Within a couple of minutes you should see lines like: ``` discovered N communities to seed seeding N communities some-community.bso updated 2 minutes ago, page cids: 0, post updates cids: 3, ... some-community.bso queueing pubsub routing provide bafkrei... some-community.bso pinned Qm... in 1.2s ``` That's it — you're seeding. The container bundles its own Bitsocial daemon (Kubo IPFS + PKC), discovers communities from the official [5chan](https://github.com/bitsocialnet/lists/tree/master/5chan-directories) and [Seedit](https://github.com/bitsocialnet/lists/tree/master/seedit-directories) directory sources, and pins their content. It re-reads both sources on the normal discovery interval, so communities added to either directory are seeded without another `bitsocial-seeder` upgrade. It also seeds the [5chan directory votes](#seed-pubsub-votes-directory-contests) by default, so you will see votes lines alongside the community ones: ``` votes joined 63 contests votes fetch serve 5chan-dir-g (12 bundles) ``` Votes seeding starts an embedded libp2p node that wants two open ports (`6742`/`6743`) and does a few minutes of AutoTLS setup on first run. Set `VOTES_MANIFEST_SOURCES=none` to switch it off and seed communities only. Directory voting is still on **testnet** — the contests are gated by the `5chan Pass` ERC-721 on Base Sepolia, so seeding them costs only the node and some testnet RPC reads. **3. (Optional) Cap the workload on small VPSes:** By default there is no cap — the seeder seeds every community in the configured public lists: ```sh MAX_COMMUNITIES=10 PIN_CONCURRENCY=1 docker compose up -d ``` See [VPS Sizing](#vps-sizing) for capacity guidance. Compose pulls `ghcr.io/bitsocialnet/bitsocial-seeder:latest` by default. To pin a specific version, edit `docker-compose.yml` and set `image: ghcr.io/bitsocialnet/bitsocial-seeder:0.6.1`. ### Run without Docker (npm) For local testing or Node-first operators (Node 24+ required): ```sh npx @bitsocial/bitsocial-seeder ``` Or install globally: ```sh npm install -g @bitsocial/bitsocial-seeder bitsocial-seeder ``` Same environment variables as the Docker image. Reuses an already-running Bitsocial daemon when one is reachable, otherwise starts the bundled one. ### Updating If you installed with Docker Compose, update the repository and recreate the container with the latest image: ```sh cd /opt/bitsocial-seeder && git pull --ff-only && docker compose pull && docker compose up -d --force-recreate ``` The `/data` Docker volume is preserved, so the seeder database and bundled daemon data survive the update. If you installed globally with npm, update to the latest release with: ```sh npm install -g @bitsocial/bitsocial-seeder@latest ``` Restart the running `bitsocial-seeder` process after an npm update. Installing the new package does not restart an existing process automatically. ### Seed a different list of communities Override `COMMUNITY_LIST_SOURCES` with one or more comma-separated URLs or local file paths pointing at JSON files in the format `{"communities": [{"address": "...", "publicKey": "..."}]}`. See [Configuration](#configuration) for the full list of env vars. ### Add private communities to seed Set `COMMUNITY_EXTRA_LIST_SOURCES` to add operator-specific communities without replacing the default public lists: ```sh COMMUNITY_EXTRA_LIST_SOURCES=/data/extra-communities.json docker compose up -d ``` Example `/data/extra-communities.json`: ```json {"communities": [{"address": "my-community.bso", "publicKey": "12D3KooW..."}]} ``` Extra sources use the same format as `COMMUNITY_LIST_SOURCES`, can be URLs, files, or directories of JSON files, and are re-read on the normal discovery interval. `MAX_COMMUNITIES` caps only the public list entries; explicitly configured extra communities are always included. If an extra entry has the same `publicKey` or address as a public entry, the extra entry wins. ### Verify what's being seeded The seeder's state lives in a SQLite database at `SEEDER_DB_PATH` (default `/data/seeder.db` in Docker): ```sh docker compose exec bitsocial-seeder sqlite3 /data/seeder.db \ 'SELECT address FROM communities' ``` See [State](#state) for the schema and other tables you can query. ### Seed pubsub votes (directory contests) The seeder seeds [`@bitsocial/pubsub-voting`](https://github.com/bitsocialnet/pubsub-voting) directory contests (e.g. 5chan's board-slot voting) **by default**, from the published 5chan manifest: ```sh # the default — no configuration needed VOTES_MANIFEST_SOURCES=https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directory-criteria.jsonc ``` It is on by default because a directory contest is only as live as the seeders holding its checkpoint: browser voters cannot dial each other, so a cold-joining voter with no reachable seeder sees an empty tally. Override the variable to seed a different manifest (`{ defaults, contests }` JSONC/JSON, one derived criteria document per slot), or set it to `none` to opt out: ```sh VOTES_MANIFEST_SOURCES=none docker compose up -d ``` With a manifest configured the seeder starts an embedded libp2p/Helia node for the votes mesh — the daemon's Kubo cannot fill this role over RPC (no topic validators, no peer scoring, no libp2p-fetch registration), so votes seeding is **Helia-only**: verified vote bundles and checkpoint chunks persist in the node's own on-disk blockstore (`VOTES_BLOCKSTORE_PATH`), and each contest's checkpoint snapshot persists under `VOTES_DATA_PATH` so a restart keeps the tally, with no Kubo involvement. The seeder then: - joins every derived contest read-only (it never publishes a ballot, so it carries no voting identity at all) and keeps the set reconciled against the manifests every `VOTES_RECONCILE_INTERVAL_MS`, - serves checkpoint root records over libp2p-fetch to cold-joining voters (this registration is automatic on join), - announces its votes peer as the provider of each contest's criteria CID, checkpoint root, and chunk CIDs on the Routing V1 HTTP routers (`VOTES_HTTP_ROUTER_URLS`), which is how voters' `findProviders()` discovers it — the library's built-in announcer re-announces hourly and debounces on joins and checkpoint changes. Browser voters can only dial **WSS** (and browsers cannot dial each other — the gossipsub mesh forms through publicly dialable seeders), so the node runs **AutoTLS** (`libp2p.direct`): the node learns its public address the same way the daemon's Kubo does (identify observed-addresses from the bootstrap connections, confirmed by AutoNAT dial-backs), then the ACME broker issues a real TLS certificate and the node announces a browser-dialable `/dns4/.libp2p.direct/.../tls/ws` address — no reverse proxy, no manual multiaddr config. The certificate takes a few minutes on first run (ACME + DNS propagation) and persists in `VOTES_DATASTORE_PATH` across restarts; watch the log for `AutoTLS certificate provisioned` and the announced addrs. Open `VOTES_LIBP2P_TCP_PORT` and `VOTES_LIBP2P_WS_PORT` in the firewall. Nothing in the votes path needs a Bitsocial daemon, but a **reachable Kubo (`KUBO_RPC_URL`) makes it faster**, and the seeder polls for one every 5 minutes rather than only at startup, so it picks up a Kubo that appears, restarts, or moves later: - **it borrows Kubo's confirmed public IP** onto the votes ports. Behind provider NAT the machine's own interfaces carry only private addresses, and js-libp2p's AutoNAT — fed by just the four bootstrap connections — can take a long time to confirm a public one, which stalls both the router announces (private addrs are dropped) and AutoTLS (it waits for a confirmed address). Kubo, with far more peers, has usually settled this already. Look for `announcing the daemon Kubo's confirmed public IP(s)` in the log. This assumes the NAT forwards the votes ports to the same place it forwards Kubo's, which holds on 1:1 provider NAT and open-firewall hosts; if it does not, AutoNAT dial-backs revalidate the borrowed address after a few minutes and drop it. - **it serves Kubo's browser-dialable addrs** to connected voters over the `bitsocial-seeder/peers` fetch key, so a browser can start dialing the community-content node while its votes checkpoint pull is still running (~1.4s saved on the first leaderboard). This is worth having on a votes-only seeder too: what matters is that *some* community-serving Kubo is reachable at `KUBO_RPC_URL`, not that this process is the one pinning the communities. Both are best-effort. With no Kubo reachable the seeder logs nothing about it and votes seeding runs normally — AutoNAT just takes longer to make the node publicly announceable. The votes peer identity persists in `VOTES_PEER_KEY_PATH` so announced provider records (and the AutoTLS domain, which embeds the peer id) stay valid across restarts — treat that key file and the `votes-keychain.pass` next to it as part of the seeder's state. Chain verification reads each contest's gate on-chain. Since pubsub-voting 0.5.0 a contest names its chain once, as a numeric `bucketChainId` — there is no ticker in the document at all, and RPC endpoints are each client's own setting (so operators can swap endpoints without forking topics). Multiple URLs per chain are queried **in parallel** (every request races all endpoints, first success wins — a dead RPC costs nothing); ETH mainnet defaults to the same six public RPCs bitsocial-cli hardcodes for pkc-js, other chains default to their viem chain's public RPC, and a busy public seeder should point `VOTES_CHAIN_RPC_URLS` (JSON, keyed by chain id, e.g. `'{"84532":["https://my-base-sepolia-rpc"]}'` for the published 5chan manifest) at its own. Ticker keys from before 0.5.0 (`baseSepolia`, `base`, `eth`) still resolve, so an existing deployment's config keeps working across the bump; the chain id is the spelling to write new config in. Votes carry community names whose claims are verified through `.bso` resolution (an ETH mainnet read) — `VOTES_ETH_RPC_URLS` sets the ETH mainnet RPCs used for both name resolution and eth-gated contest verification (an explicit `VOTES_CHAIN_RPC_URLS` `"eth"` entry still wins for verification), defaulting to the same resolver providers bitsocial-cli gives pkc-js; a seeder whose resolvers are down counts (and therefore serves) almost nothing. The log answers the questions production debugging asks — did a voter ever connect (`votes conn open`), join a topic (`votes topic subscribe`), pull the checkpoint (`votes fetch serve`, with the decoded bundle count — a root record is constant-size whether the contest is empty or not, so only the decoded `count` distinguishes "no votes" from "checkpoint didn't load"), or publish a vote (`votes gossip ... live vote bundle`)? ### Run a votes-only seeder Set `COMMUNITY_LIST_SOURCES=none` to skip community seeding entirely and dedicate the machine to directory contests: ```sh COMMUNITY_LIST_SOURCES=none docker compose up -d ``` The seeder then never runs discovery, subscribes to no community pubsub topics, and pins nothing — it boots straight into the votes workers and logs `community seeding disabled (COMMUNITY_LIST_SOURCES=none), seeding votes only (no bitsocial daemon needed)`. `none` is what distinguishes "zero sources" from "unset" (an unset or empty variable falls through to the defaults), and the same spelling works for `VOTES_MANIFEST_SOURCES` to get the mirror config: communities only, no votes. Setting *both* to `none` leaves nothing to seed, and the seeder exits with `nothing to seed`. **A votes-only seeder never requires a Bitsocial daemon and never starts one.** Only the community half talks to a daemon (PKC RPC for community updates, Kubo for pinning and pubsub), so `COMMUNITY_LIST_SOURCES=none` is also the switch that takes the daemon out of the picture: no PKC RPC probe, no `SEEDER_DAEMON_AUTOSTART`, no bundled daemon, no `SEEDER_DAEMON_DATA_PATH` on disk. That matters on a machine where something else owns the daemon: before this, a votes-only container restarting during daemon maintenance would spawn its own bundled daemon, claim the PKC RPC port, and block the real daemon from coming back. So on a votes-only seeder: | variable | status | | --- | --- | | `COMMUNITY_LIST_SOURCES=none` | the switch | | `KUBO_RPC_URL` | **optional, still useful** — the public-IP borrow and the browser pre-warm hint above, both best-effort and polled | | `PKC_RPC_URL`, `IPFS_GATEWAY_URL` | unused | | `SEEDER_DAEMON_AUTOSTART`, `SEEDER_DAEMON_DATA_PATH`, `SEEDER_DAEMON_LOG_PATH`, `SEEDER_DAEMON_READY_*` | never consulted | | `VOTES_*` | the configuration that matters | Only `VOTES_LIBP2P_TCP_PORT` and `VOTES_LIBP2P_WS_PORT` need to be open inbound. Keep `network_mode: host` (the compose default on Linux) even with no daemon: it publishes those two ports *and* lets the container reach a host Kubo on `127.0.0.1`, which bridge networking would not. If you are switching an existing seeder to votes-only, the bundled daemon's data directory is left behind and is safe to delete once nothing is seeding communities — `/data/bitsocial` and `/data/logs` inside the volume, per `SEEDER_DAEMON_DATA_PATH` / `SEEDER_DAEMON_LOG_PATH`. ## Configuration The default config expects: - PKC RPC: `ws://127.0.0.1:9138` - Kubo RPC: `http://127.0.0.1:50019/api/v0` - community lists: official `bitsocialnet/lists` 5chan and Seedit directory files - daemon data: `/data/bitsocial` On Linux hosts the compose file uses `network_mode: host`, so the container can reach the host daemon through `127.0.0.1`. If no host daemon is running, the container starts its bundled daemon on those same local RPC addresses. **A daemon that is down does not stop a combined seeder from seeding votes.** On a seeder running both halves, the daemon is a dependency of the community half only, so an unreachable or slow-to-start daemon no longer exits the process — the votes workers start immediately and the daemon is retried in the background (10s, doubling to 5 minutes), logging `community seeding is DOWN … votes seeding is unaffected` on each attempt. The community workers start as soon as it comes up. This matters under `restart: unless-stopped`: exiting used to crash-loop the container, and every restart re-ran the votes cold join and re-announced to the routers, degrading votes seeding for a reason unrelated to it. A **communities-only** seeder still exits on a daemon failure, because nothing would be left running. Useful environment overrides: ```sh # Community seeding only; a votes-only seeder never opens the PKC RPC PKC_RPC_URL=ws://127.0.0.1:9138 # Required for community seeding; optional (but useful) for votes — see "Run a votes-only seeder" KUBO_RPC_URL=http://127.0.0.1:50019/api/v0 # Kubo RPC used for pubsub subscriptions; defaults to KUBO_RPC_URL PUBSUB_KUBO_RPC_URL=http://127.0.0.1:50019/api/v0 IPFS_GATEWAY_URL=http://127.0.0.1:6473 # Default; 'none' disables community seeding (votes-only seeder, which needs no daemon) COMMUNITY_LIST_SOURCES=https://api.github.com/repos/bitsocialnet/lists/contents/5chan-directories?ref=master,https://api.github.com/repos/bitsocialnet/lists/contents/seedit-directories?ref=master COMMUNITY_EXTRA_LIST_SOURCES=/data/extra-communities.json # How often the community list sources are re-read ("the discovery interval"); default 10 minutes DISCOVER_INTERVAL_MS=600000 # Minimum time between re-providing a community's pubsub routing CIDs; default 6 hours PUBSUB_ROUTING_PROVIDE_INTERVAL_MS=21600000 # Only consulted when community seeding is on; a votes-only seeder never autostarts a daemon SEEDER_DAEMON_AUTOSTART=true SEEDER_DAEMON_DATA_PATH=/data/bitsocial SEEDER_DAEMON_LOG_PATH=/data/logs # How long to wait for an autostarted daemon's RPCs to come up, and how long they must # stay up before the daemon counts as ready SEEDER_DAEMON_READY_TIMEOUT_MS=120000 SEEDER_DAEMON_READY_STABLE_MS=2500 SEEDER_DB_PATH=/data/seeder.db # Legacy JSON state file migrated into the database on first start; the mirror write can # be disabled with SEEDER_STATE_WRITE_FILE=false SEEDER_STATE_PATH=/data/seederState.json SEEDER_STATE_WRITE_FILE=true # No default cap — all discovered public-list communities are seeded unless this is set MAX_COMMUNITIES= # Default 2; the Docker image and compose file set 1 PIN_CONCURRENCY=2 SEEDER_UPDATE_CHECK_ENABLED=true SEEDER_UPDATE_CHECK_INTERVAL_MS=86400000 SEEDER_UPDATE_CHECK_TIMEOUT_MS=5000 # Default; 'none' disables votes seeding (communities-only seeder) VOTES_MANIFEST_SOURCES=https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directory-criteria.jsonc VOTES_HTTP_ROUTER_URLS=https://peers.pleb.bot,https://routing.lol,https://peers.forumindex.com,https://peers.plebpubsub.xyz,https://routerofbitsocial.xyz,https://bsotracker.online VOTES_LIBP2P_HOST=0.0.0.0 VOTES_LIBP2P_TCP_PORT=6742 VOTES_LIBP2P_WS_PORT=6743 VOTES_RECONCILE_INTERVAL_MS=600000 # Per-chain RPC override, keyed by chain id; the published 5chan manifest counts in Base Sepolia VOTES_CHAIN_RPC_URLS='{"84532":["https://sepolia.base.org"]}' VOTES_ETH_RPC_URLS=https://eth.drpc.org,https://ethereum-rpc.publicnode.com VOTES_PEER_KEY_PATH=/data/votes-peer.key VOTES_BLOCKSTORE_PATH=/data/votes-blockstore VOTES_DATASTORE_PATH=/data/votes-datastore VOTES_DATA_PATH=/data/votes-cache VOTES_FETCH_MAX_STREAMS=256 VOTES_UPDATE_CONCURRENCY=8 # util.inspect depth for object logging (unset = Node's util.inspect default) DEBUG_DEPTH=6 ``` ### Public seeder defaults `COMMUNITY_LIST_SOURCES` points at the GitHub contents APIs for both `bitsocialnet/lists/5chan-directories` and `bitsocialnet/lists/seedit-directories`. The seeder re-reads every non-default JSON file from both folders on the normal discovery interval. New directory files and changes to existing files therefore reach current seeders without another package release or restart. Older releases and old Docker Compose files only poll `5chan-directories`, because Compose sets this environment variable explicitly. For compatibility, public seed targets that those installs must see can temporarily be mirrored in `bitsocialnet/lists/5chan-directories/bitsocial-seeder-communities.json`. Existing seeders fetch every JSON file in that folder except `*-defaults.json`, while 5chan clients and stats tooling only treat files named `5chan--directory.json` as real 5chan directories. Keep that compatibility mirror until old folder-only installs have had time to upgrade. Current installs use the two directory folders themselves as the canonical public sources, avoiding a generated aggregate that could drift from the client directory lists. ## State The seeder keeps its operational state in a single SQLite file at `SEEDER_DB_PATH` (defaults to `./seeder.db`, set to `/data/seeder.db` in the Docker image). The file holds the seeded community list, per-pin bookkeeping for stale-pin GC, the pubsub-routing re-provide throttle, and the durable work queues + scheduler powered by [honker](https://github.com/russellromney/honker). On first start the seeder will migrate any pre-existing `seederState.json` into the database. After migration the JSON file is no longer read or written and can be removed at the operator's discretion. Inspect state with the host's `sqlite3` against the file directly, e.g. `sqlite3 /data/seeder.db 'SELECT community_key, address FROM communities'`. The seeder checks npm for a newer `@bitsocial/bitsocial-seeder` release on startup and once per day after that. When a newer release exists, it prints an update notice in the logs. Set `SEEDER_UPDATE_CHECK_ENABLED=false` to disable that check. The same check also watches the bundled runtime packages, including `@bitsocial/bitsocial-cli` and `@pkcprotocol/pkc-js`. Upgrading the seeder package or Docker image upgrades the bundled daemon used by `SEEDER_DAEMON_AUTOSTART=true`. If the seeder is reusing an already-running external `bitsocial daemon`, upgrade and restart that daemon separately; the seeder does not install over or restart externally managed daemon processes. ## VPS Sizing For public seeding, size the host like a small Kubo node plus a lightweight Node.js seeder process. The seeder wrapper is small, but with `SEEDER_DAEMON_AUTOSTART=true` it also runs a bundled Bitsocial daemon and Kubo IPFS node. A [votes-only seeder](#run-a-votes-only-seeder) runs neither, so it needs far less — the embedded votes node plus its blockstore and checkpoint snapshots, which is a small fraction of these numbers. Recommended starting point: - CPU: 2 vCPU. - Memory: 4 GiB can work for a low-cost trial, especially with swap, but 6 GiB or more is the safer target for unattended public seeders. Kubo's [published baseline](https://docs.ipfs.tech/install/command-line/#system-requirements) is 6 GiB memory and 2 CPU cores. - Disk: 20 GiB minimum free space for Docker, logs, seeder state, and the IPFS repo; 50 GiB or more is more comfortable for long-running nodes or larger `MAX_COMMUNITIES` values. - Network: stable public IPv4 or IPv6 with unrestricted outbound TCP/UDP. Allow inbound Kubo swarm traffic if possible, usually TCP/UDP 4001 with the default Kubo config, but keep PKC and Kubo RPC ports private to the host. - Transfer: avoid tiny metered bandwidth caps. Start with at least 1 TB/month included transfer and monitor provider-level bandwidth, not only `ipfs stats bw`. The compose file ships with memory guardrails: `NODE_OPTIONS: "--max-old-space-size=1024"` caps the seeder's V8 heap (Node's default limit scales with host RAM, and lazy GC otherwise lets a long-running seeder balloon toward ~4 GiB on an 8 GiB host), and `mem_limit: 2g` is a hard container backstop that also covers native/buffer memory. Raise both if you seed many more communities than the defaults and the seeder gets memory-starved; on tighter hosts, lower `MAX_COMMUNITIES` before lowering the caps. The default community sources are dozens of small directory communities plus a short supplemental seeder list, not full media archiving. Disk and bandwidth mostly scale with `MAX_COMMUNITIES`, pinned page/update size, pubsub activity, and Kubo/libp2p overhead. On small VPSes, set `MAX_COMMUNITIES` (unset = no cap) and keep `PIN_CONCURRENCY=1` (the Docker default). Bitsocial configures delegated HTTP routing/tracker endpoints for provider lookups, so it should be lighter than an untuned Kubo node doing full DHT provider sweeps. It still runs Kubo and joins pubsub topics, so treat it as Kubo-class infrastructure rather than a static HTTP service. ## Local Development ```sh npm install npm test npm start ``` ## Releases Releases are driven by the `version` in `package.json`. On a successful push to `master`, CI updates `CHANGELOG.md`, pushes versioned Docker image tags, publishes `@bitsocial/bitsocial-seeder` to npm with trusted publishing, and creates the matching GitHub Release. The npm trusted publisher should be configured for: - npm package: `@bitsocial/bitsocial-seeder` - GitHub repository: `bitsocialnet/bitsocial-seeder` - workflow filename: `release.yml` - allowed action: `npm publish` The package must exist on npm before trusted publishing can be configured. After the first package version exists, future releases should publish through CI without long-lived npm tokens. For initial npm bootstrap, backfill historical package versions before cutting the next release if you want npm to show the full release line. Publish `0.1.0` and `0.1.1` only from their matching release code plus the minimum npm metadata needed for the scoped package; do not publish current code under an old version. ### Bitsocial Pubsub Provider Repository: bitsocialnet/pubsub-provider Source: https://github.com/bitsocialnet/pubsub-provider#readme Description: Run a Bitsocial pubsub fallback provider with a bundled Kubo node. # Bitsocial Pubsub Provider Run a Bitsocial pubsub fallback provider with a bundled Kubo node. Modern Bitsocial apps such as 5chan use pure p2p in the browser by default. This provider is still useful as an optional fallback for clients that disable browser p2p, need a non-p2p pubsub relay, or want a public gateway/provider bundle for compatibility. ## What It Runs - a public HTTP proxy for pubsub, gateway, name-provider, and delegated routing routes - a bundled Kubo node with pubsub enabled - a delegated HTTP routing provider at `/routing/v1/providers` - optional ENS/SNS proxy helpers when chain RPC environment variables are configured It includes the delegated HTTP routing compatibility endpoint that old provider clients used for provider lookup/provide requests. You can still run a dedicated tracker separately if you want that role split out. ## Ports The defaults are chosen so this can run next to `bitsocial-seeder` on the same host. | Purpose | Default | Notes | | --- | ---: | --- | | Public HTTP proxy | `8000` in the app, `80` in Docker Compose | Set `PUBSUB_PROVIDER_HTTP_PORT` for the host port in Compose. | | Kubo swarm | `4002` TCP/UDP | Avoids the seeder's default Kubo swarm port `4001`. | | Kubo API | `5001` local only | Used internally by the proxy. | | Kubo gateway | `8080` local only | Used internally by the proxy. | ## Quick Start The published Docker image includes the Kubo binary, so normal Docker users do not need to download Kubo during container startup. ```sh git clone https://github.com/bitsocialnet/pubsub-provider.git cd pubsub-provider docker compose pull docker compose up -d ``` The default Compose file runs the pinned published image. To build from local source instead: ```sh docker compose -f docker-compose.yml -f docker-compose.build.yml up --build -d ``` Check logs: ```sh docker logs --follow pubsub-provider ``` Test the proxy: ```sh curl http://127.0.0.1/commit-hash ``` ## Configuration Useful environment overrides: ```sh PUBSUB_PROVIDER_HTTP_PORT=80 PUBSUB_PROVIDER_SWARM_PORT=4002 PUBSUB_PROVIDER_PORTS=8000 KUBO_RPC_URL=http://127.0.0.1:5001/api/v0 IPFS_GATEWAY_URL=http://127.0.0.1:8080 HTTP_ROUTER_URLS=https://example-router.invalid PUBSUB_PROVIDER_ROUTING_STORE_PATH= BASIC_AUTH_USERNAME= BASIC_AUTH_PASSWORD= IPFS_GATEWAY_USE_SUBDOMAINS=false SHUTDOWN_KEY= ETH_PROVIDER_URL= ETH_PROVIDER_URL_WS= SOL_PROVIDER_URL= ``` Source/local runs download Kubo automatically when `bin/ipfs` is missing. If your host has unreliable DNS or blocks the default download domains, you can provide comma-separated full URLs or base URLs: ```sh PUBSUB_PROVIDER_KUBO_DOWNLOAD_URLS=https://dist.ipfs.tech/kubo,https://github.com/ipfs/kubo/releases/download PUBSUB_PROVIDER_KUBO_DOWNLOAD_ATTEMPTS=5 PUBSUB_PROVIDER_KUBO_DOWNLOAD_TIMEOUT_MS=600000 ``` If you also run `bitsocial-seeder`, keep this provider on swarm port `4002` or another non-`4001` port. ## Upgrading If you previously ran `latest`, force Compose to recreate the container from the pinned image: ```sh git pull docker compose down docker compose pull docker compose up -d --force-recreate ``` Verify the fixed image is running: ```sh docker inspect pubsub-provider --format 'image={{.Image}} restarts={{.RestartCount}}' docker exec pubsub-provider /app/bin/ipfs version curl http://127.0.0.1/commit-hash ``` The logs should not contain `downloading ipfs`. If they do, the container is still running an old local image or an old checkout. ## Local Development Requires Node.js 24 or newer. ```sh npm install npm start -- [--ipfs-gateway-use-subdomains] [--shutdown-key ] ``` Run tests: ```sh npm test ``` ## Cloudflare Note Pubsub subscriptions use long-lived HTTP connections. Cloudflare can time out those keepalive connections unless response buffering/streaming behavior is configured appropriately. If pubsub fallback is the reason you run this provider, prefer direct DNS or infrastructure that supports long-lived streaming responses. ## License GPL-3.0-or-later. See [LICENSE](https://github.com/bitsocialnet/pubsub-provider/blob/HEAD/LICENSE). ### bitsocial-indexer Repository: bitsocialnet/bitsocial-indexer Source: https://github.com/bitsocialnet/bitsocial-indexer#readme Description: A neutral, self hostable crawler + search/index API + web UI for the # bitsocial-indexer A neutral, self-hostable **crawler + search/index API + web UI** for the [Bitsocial](https://bitsocial.net) network. It connects to a [`bitsocial-cli`](https://github.com/bitsocialnet/bitsocial-cli) daemon over PKC RPC, indexes the communities **you** configure into a local SQLite database, and exposes them through a REST + full-text-search API and an optional server-rendered web UI. > **It ships empty.** Out of the box the indexer knows about **zero** > communities and shows nothing — the operator decides what to index. Point it > at a list of communities and it becomes a search engine / archive for exactly > those. This is the engine. A concrete deployment — choosing which communities to index, re-skinning the UI, adding ads or analytics — is layered on top as a separate project (see [Running your own instance](#running-your-own-instance)). --- ## Architecture ``` Bitsocial network (IPFS / IPNS / pubsub) │ bitsocial-cli daemon (PKC RPC, ws://localhost:9138) │ @pkcprotocol/pkc-js ┌──────────┴───────────────────────────────────┐ │ server/ — crawler + API (one Node service) │ │ crawler ──▶ SQLite + FTS5 ──▶ Fastify API │ └──────────┬───────────────────────────────────┘ │ REST + search (the integration seam) ┌────────┴────────┐ │ │ webui/ any external client (Next.js, (e.g. a Bitsocial app's bitsocial.net in-app /search board just skin) calls the API) ``` The **API is the product surface.** The bundled `webui` is one consumer; a Bitsocial client adding in-app search is another — it just calls the same endpoints, no shared frontend code. | Part | Stack | |------|-------| | `server/` | Node 22, TypeScript (ESM), [Fastify](https://fastify.dev) 5, [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) + FTS5, [`@pkcprotocol/pkc-js`](https://github.com/pkcprotocol/pkc-js) | | `webui/` | Next.js 15 (App Router, SSR for SEO), React 19, Bitsocial brand tokens | ## Quickstart ```bash # 1. API server (http://localhost:4000) cd server npm install npm run seed # optional: load demo communities + posts so the UI isn't empty npm run dev # 2. Web UI (http://localhost:3000) — in another terminal cd webui npm install npm run dev ``` Without `npm run seed`, the server starts with **no communities** and the UI shows its empty / onboarding state — which is the real default. Configure communities (below) to index live content. ## Configuration All config is environment variables (see [`server/.env.example`](https://github.com/bitsocialnet/bitsocial-indexer/blob/HEAD/server/.env.example)). ### `server/` | Var | Default | Meaning | |-----|---------|---------| | `COMMUNITIES` | _(empty)_ | Comma-separated community addresses to index, e.g. `art.bso,tech.bso` | | `COMMUNITIES_SOURCE` | _(empty)_ | URL/path to a JSON list of community addresses (e.g. a client's directory). Overrides/augments `COMMUNITIES`. | | `PKC_RPC_URL` | `ws://localhost:9138` | The `bitsocial-cli` daemon RPC endpoint | | `DB_PATH` | `./data/indexer.db` | SQLite file (`:memory:` for ephemeral) | | `CRAWL_INTERVAL_MS` | `60000` | Per-community delay before the next refresh | | `CRAWL_CONCURRENCY` | `4` | Maximum communities crawled at once | | `CRAWL_TIMEOUT_MS` | `300000` | Hard timeout for one community crawl; a timeout resets the RPC client | | `ALLOWED_ORIGINS` | `*` | CORS allow-list, comma-separated (`*` = any origin — fine for a public read-only API). An entry may contain `*` as a wildcard, e.g. `https://*.seedit.localhost` matches every branch-scoped dev origin. `CORS_ORIGIN` is accepted as a legacy fallback. | | `BLOCKLIST_SOURCE` | _(empty)_ | Path to a JSON file of CIDs to take down (operator blocklist, see below). | | `DIRECTORY_DEFAULTS_SOURCE` | _(empty)_ | URL/path to a client's `*-directories-defaults.json`, which states `features.safeForWork` per directory code (see below). | | `NSFW_OVERRIDES_SOURCE` | _(empty)_ | Path to a JSON file of operator NSFW verdicts per community (see below). | If neither `COMMUNITIES` nor `COMMUNITIES_SOURCE` is set, the crawler stays idle and the indexer serves nothing. That is intentional. #### Takedowns (`BLOCKLIST_SOURCE`) An archive keeps serving content after it disappears from the source network, so upstream moderation can no longer reach it — takedown requests (DMCA, illegal content) need an operator-side mechanism. Point `BLOCKLIST_SOURCE` at a JSON file where each entry is a bare CID string or `{ "cid": "…", "scope": "comment" | "thread", "reason": "…" }` (`scope` defaults to `comment`; `thread` takes down a post **and all its replies** by the post's CID). Add a CID to the file and it is redacted within a minute — the file is re-read whenever it changes, no restart needed; remove the entry and the stored content is served again (the redaction never destroys the archived data). Blocklisted comments leave listings and search but stay in threads as redacted tombstones, marked `takedown: 1` (plus the optional `takedown_reason`) on the API so UIs can distinguish them from upstream moderation, and they stay redacted across re-crawls. The bundled web UI shows them as `[removed — takedown request]` and documents the policy on its `/legal` page (see `CONTACT_EMAIL` below). #### NSFW communities A community declares its own status through the protocol: `community.features.safeForWork` is an **optional** boolean, so it has three states — `true`, `false`, and never set. `safeForWork === false` is the owner-declared way of saying "this community is NSFW". Each indexed community gets both the raw declaration (`safe_for_work`: `1`, `0` or `null`) and a resolved `nsfw` flag on `GET /api/communities` (and `/api/communities/:address`). The resolution takes four signals, **highest precedence first**: 1. **Operator override** — `NSFW_OVERRIDES_SOURCE`, a JSON file where each entry is a bare address or `{ "address": "…", "nsfw": false, "reason": "…" }`. `nsfw` defaults to `true`, and an explicit `false` clears the flag, so a bad verdict is correctable. Like the blocklist, the file is re-read whenever it changes — no restart needed. 2. **`community.features.safeForWork`** — the owner's own declaration, read off the community on every crawl. `false` → NSFW, `true` → not, unset → the next signal decides. 3. **The directory the address is listed under** — `DIRECTORY_DEFAULTS_SOURCE` (below), for the many communities whose owner never set the feature. 4. **Inference from content** — any indexed comment in the community carrying the protocol's `nsfw` flag means the community accepts NSFW content. An unset `safeForWork` is never read as either verdict: it falls through, and only the last signal (inference) is allowed to answer with a plain boolean. ##### Directory verdicts (`DIRECTORY_DEFAULTS_SOURCE`) Bitsocial clients state a *directory's* SFW status once, by directory code, in their `-directories-defaults.json` — this is how 5chan knows `/f/` and `/b/` are NSFW while `/3/` and `/a/` are not. The sibling `--directory.json` files hold that directory's candidate addresses. Point `DIRECTORY_DEFAULTS_SOURCE` at the defaults file and the crawler reads both, giving every address listed under a directory that directory's `features.safeForWork`: ``` DIRECTORY_DEFAULTS_SOURCE=https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directories/5chan-directories-defaults.json ``` Sibling file names follow the convention that repo documents, so one setting reaches every directory. A directory that states no `safeForWork`, or an address in no directory at all, contributes nothing and falls through to inference. Both files are read once, when the crawler schedules its communities. `GET /api/search?nsfw=` filters on the result: `false` (**the default**) drops anything NSFW — the comment is flagged, or its community is — and `true` includes it. Listings (`/api/posts`) and the sitemap are not filtered. ### `webui/` | Var | Default | Meaning | |-----|---------|---------| | `INDEXER_API` | `http://localhost:4000` | Where the UI reads the API from (server-side fetch) | | `SITE_NAME` | `Bitsocial` | Instance name in the header / page titles | | `SITE_BADGE` | `Indexer` | Small pill next to the name (empty to hide) | | `SITE_URL` | `http://localhost:3000` | Public origin of the web UI — canonical URLs, OpenGraph tags, `robots.txt`, sitemaps | | `THEME` | `default` | UI skin: `default` (Bitsocial dark) or `5chan` (classic imageboard look) | | `BRAND_TEXT` | _(empty)_ | Optional footer attribution line, e.g. `A Bitsocial Forge product`. Unset = nothing rendered | | `BRAND_URL` | _(empty)_ | Makes `BRAND_TEXT` a link | | `CONTACT_EMAIL` | _(empty)_ | Contact address for content-removal / takedown requests, shown on the `/legal` archive-policy page. Unset = the page says requests are handled by the instance operator | | `SHOW_NSFW` | `false` | Whether this instance's search returns NSFW results. The UI sends the value on every query, so an archive of boards that are NSFW by design opts in with `SHOW_NSFW=true` rather than silently inheriting the API's safe default | The web UI serves its own `robots.txt` and a `sitemap.xml` **sitemap index** (one child sitemap per community, capped at the 5,000 most recent posts each, enumerated through the paginated `/api/posts` listing and cached for an hour). ## API CORS-enabled so browser clients can call it directly. | Endpoint | Description | |----------|-------------| | `GET /api/health` | Status + index counts | | `GET /api/communities` | Indexed communities + post counts + resolved `nsfw` flag + declared `safe_for_work` | | `GET /api/posts` | Browse posts — `?community=&sort=new\|top\|replies\|old&time=hour..all&page=&limit=&replies=true` | | `GET /api/posts/:cid` | A thread: original post + threaded replies | | `GET /api/search` | Full-text search — `?q=&community=&sort=&time=&page=&limit=&replies=&nsfw=`, plus the advanced filters below (NSFW excluded by default). A `q` that is a comment CID is looked up by CID instead (see below) | | `GET /sitemap.xml`, `/robots.txt` | SEO | ### Advanced search filters `/api/search` also accepts the five old.reddit-style filters a client parses out of its search box, so they no longer have to be passed through as literal query text. Each is optional, and they AND together — with each other and with `q`, `community`, `time` and `nsfw` — so a query only ever narrows: | Param | Prefix | Matches | |-------|--------|---------| | `author` | `author:lena.bso` | Exactly (case-insensitively) the author's address **or** display name. Never a prefix or substring: an author is an identity, so `lena` must not stand in for `lena-imposter.bso` | | `site` | `site:example.com` | The link's parsed **host**, subdomains included (`www.example.com` counts). Never a substring of the URL, so `https://evil.com/?r=example.com` is not a match | | `url` | `url:ink-study` | A substring of the whole link — the path / slug / query search that `site` deliberately is not | | `selftext` | `selftext:tokenizer` | Words in the post body, through the same full-text index `q` uses | | `self` | `self:yes` / `self:no` | `yes` = text posts only, `no` = link posts only. Three-state: **absent means no opinion** and keeps both | `q` may be empty when at least one filter is set: `?author=lena.bso` is a complete query, answered newest-first since there is no relevance to rank. `?community=…` alone is not — narrowing parameters need something to narrow, and `/api/posts` is the listing endpoint. Only `/api/search` takes these; `/api/posts` and the sitemap are unchanged. ### Searching by CID A comment CID pasted into a search box is one opaque token the full-text index can never match, so when `q` is exactly one CID — CIDv0 (`Qm…`) or CIDv1 (`bafy…`), recognised by parsing it rather than by pattern — `/api/search` looks it up by `comments.cid` instead and returns the comment as an ordinary result (`total: 1`); a client that already renders search results needs no change. Only the exact `cid` matches: a CID that appears solely as a thread or parent reference names a comment that was never indexed. Every other parameter still narrows — `community=` must be the comment's own, `nsfw=false` still hides an NSFW comment (a pasted CID is not a way around an instance's safe default) — and, like `/api/posts/:cid`, a removed, deleted or taken-down comment comes back as its redacted tombstone, though only for the bare lookup: with `author`, `site`, `url`, `self` or `selftext` set, tombstones stay hidden as in text search, since a yes/no answer over redacted content would leak it. A CID mixed with other words is an ordinary text search. ## Running your own instance `bitsocial-indexer` is a **tool, not a hosted service** — there is no central instance. To run one: 1. Deploy this engine (Docker, a VPS, etc.). 2. Set `COMMUNITIES` / `COMMUNITIES_SOURCE` to the communities you want. 3. Optionally re-skin `webui` (override the theme tokens in [`webui/app/globals.css`](https://github.com/bitsocialnet/bitsocial-indexer/blob/HEAD/webui/app/globals.css)) and add your own branding, ads, or analytics in your own deployment repo. Because this engine is **GPL-3.0-or-later** (copyleft on *distribution*, not on running a network service), you can run a modified, private, monetised instance without publishing your changes — the same way Etherscan is a closed service built on open Ethereum. ## License [GPL-3.0-or-later](https://github.com/bitsocialnet/bitsocial-indexer/blob/HEAD/LICENSE). Brand assets belong to Bitsocial Forge. ### bitsocial-previewer Repository: bitsocialnet/bitsocial-previewer Source: https://github.com/bitsocialnet/bitsocial-previewer#readme Description: Link preview + redirect server for Bitsocial clients. When someone shares a # bitsocial-previewer Link-preview + redirect server for Bitsocial clients. When someone shares a post link on Twitter/Telegram/Discord/etc., this server renders the OpenGraph/Twitter card for that post, then redirects real browsers into the app. It is **multi-tenant**: one running instance serves many clients and many mirror domains, picking the right profile from the request's `Host` header — so a single deployment (and a single TLS setup) covers every client and every mirror. ``` ┌──────────── s.5chan.app/biz/thread/ share link ───────► │ bitsocial-previewer ──► OG/Twitter card (for scrapers) (no #/ hash) └──────────── 302-ish JS redirect ──► 5chan.app/#/biz/thread/ ``` ## How it works 1. 5chan copies **path-based** share links (no `#/` hash) so the path reaches the server: `https://s.5chan.app//thread/`. 2. The server matches the path to a client profile + route, fetches the post by cid via `@pkcprotocol/pkc-js` (`getComment`), and builds the card. 3. Browsers get redirected into the app's hash route; scrapers read the tags. `` is usually a short **directory code** (e.g. `biz`). We pass it straight through to the app (which resolves it); the cid is authoritative for the preview, so the previewer never needs to resolve codes itself. ### Recognised share-link formats (5chan) | Incoming on `s.5chan.app` | Preview | Redirects to | |---|---|---| | `//thread/` | rich (title/text/image) | `5chan.app/#//thread/` | | `//catalog` | generic board card | `5chan.app/#//catalog` | | `/` | generic board card | `5chan.app/#/` | | `/p/
/c/` (legacy) | rich | `5chan.app/#/
/thread/` | Unresolvable cid → still redirects to the app with a generic card (short cache), so a share link never dead-ends. ## Layout | File | Purpose | |---|---| | [`lib/clients.js`](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/lib/clients.js) | **The per-client design.** Profiles (hostnames, app URL, routes) + host→profile + route matching. | | [`lib/html.js`](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/lib/html.js) | OG/Twitter tag rendering + redirect (HTML-escaped, XSS-safe). Pure. | | [`lib/pkc.js`](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/lib/pkc.js) | `@pkcprotocol/pkc-js` init + cached `getComment`. | | [`lib/media.js`](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/lib/media.js) | Comment media URL + external-link `og:image` scraping. | | [`start.js`](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/start.js) | Express wiring. | | [`config.js`](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/config.js) | Env-driven config (port, bind host, pkc options). | ## Run locally ```bash npm install npm start # listens on 127.0.0.1:3924, public-gateway fallback # in another shell: curl -s 'http://localhost:3924/biz/thread/' | grep -i og: ``` ```bash npm test # routing + HTML unit tests (no network/install needed for the pure logic) ``` ## Deploy (Caddy) Deployed under `/opt/bitsocial-previewer` like the box's other services. Config (gateways, port) is inline in [`docker-compose.yml`](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/docker-compose.yml). The previewer resolves shared comments through the public IPFS gateways the 5chan app uses — a fetch is sub-second once a gateway has the content, and a hard timeout falls back to a generic card so a request never hangs. (A box's local IPFS node generally can't resolve arbitrary shared comment cids: thinly peered, and it doesn't pin individual comment blocks.) ```bash # on the box, in /opt/bitsocial-previewer (source synced or git-cloned): docker compose up -d --build # builds + runs on 127.0.0.1:3924 docker compose logs -f --tail=50 ``` CI also publishes `ghcr.io/bitsocialnet/bitsocial-previewer:latest` ([workflow](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/.github/workflows/docker.yml)); once that package is made public you can `docker compose pull && docker compose up -d` instead of building on the box. Point the local Caddy at it with [`deploy/Caddyfile.snippet`](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/deploy/Caddyfile.snippet), then do the Cloudflare DNS change in [`deploy/cloudflare.md`](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/deploy/cloudflare.md). ## Add a client or a mirror - **New mirror domain for 5chan:** add it to the `5chan` profile's `hostnames` in [`lib/clients.js`](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/lib/clients.js), add a Caddy block + a DNS A record. - **New client (e.g. Seedit):** fill in the `seedit` profile template (hostnames, `appBaseUrl`, route formats) and enable it. ## License [GPL-3.0-or-later](https://github.com/bitsocialnet/bitsocial-previewer/blob/HEAD/LICENSE) — matching the Bitsocial core stack it builds on (`@pkcprotocol/pkc-js`, `bitsocial-react-hooks`, 5chan, Seedit). ### challenge-composer Repository: bitsocialnet/challenge-composer Source: https://github.com/bitsocialnet/challenge-composer#readme Description: Live app: ** A standalone, offline-first web app for **visualizing and editing** the `challenges` array of a PKC community's `settings` (pkc-js format). Inspired by the challenges section of seedit's community-settings view. - Ships as **one self-contained `dist/index.html`** with every JS/CSS chunk inlined by `vite-plugin-singlefile`. Drop it anywhere — USB stick, email attachment, static host — and open from `file://`. - Zero outward HTTP at runtime. Everything works from the bundled app and user-supplied data. - Fully typed, ESM, Node ≥22, Vitest for tests. ## Requirements - Node **22+** - npm 10+ ## Getting started ```sh npm ci # install pinned exact versions npm run build # type-check + emit dist/index.html (everything inlined) xdg-open dist/index.html # or just double-click it ``` Supporting scripts: `npm test` (Vitest + happy-dom), `npm run typecheck`, `npm run preview` (serves `dist/` locally for a smoke test). The current `package.json` version is injected into the bundle via Vite's `define` as `__APP_VERSION__` and rendered next to the header title, so whoever opens `dist/index.html` can see which release it came from. ## Release flow Versioning is automated. Conventional commits (`feat:`, `fix:`, `perf:`, `build:`, `revert:`) on `master` trigger a release: 1. The **CI** workflow (`.github/workflows/ci.yml`) runs typecheck, tests, and build on every push/PR. 2. The **Release and deploy** workflow (`.github/workflows/deploy.yml`) runs on successful CI completion on `master`: it invokes `release-it` (config in `config/.release-it.json`) which bumps the version, updates `CHANGELOG.md`, commits `chore(release): X.Y.Z [skip ci]`, tags `vX.Y.Z`, and cuts a GitHub release. It then rebuilds `dist/index.html` with the new version embedded and publishes it to GitHub Pages. The repo ships a commit-msg hook (`.githooks/commit-msg`, installed by `scripts/install-git-hooks.mjs`) that runs `commitlint` on the message so non-conventional commits are caught before they reach master. `git cz` via `commitizen` is wired up as an interactive alternative. State (drafts, share links) lives in `localStorage` of whichever origin you open it from. ## What it edits A JSON array of `CommunityChallengeSetting`, the same shape pkc-js accepts in `community.settings.challenges`. Each entry: ```jsonc { "name": "text-math", // pkc-js built-in challenge identifier, OR "path": "./my-challenge.js", // path to a custom challenge module (one of the two) "description": "Solve to post.", "pendingApproval": true, // mods must approve after the user solves "options": { // all values must be strings "difficulty": "2" }, "exclude": [ // any matching group lets the author bypass { "role": ["moderator", "admin", "owner"] }, { "postCount": 10, "firstCommentTimestamp": 604800, "rateLimit": 3, "rateLimitChallengeSuccess": true }, { "publicationType": { "reply": true } } ] } ``` ### Challenges shipped with pkc-js A challenge name is "built-in" iff it is a key of `PKC.challenges` (= `pkcJsChallenges`) in `@pkcprotocol/pkc-js` — these resolve by `name` without any install step on the community node. The list is extracted at build time by the `pkcBuiltinNamesPlugin` Vite plugin (see `vite.config.ts`) and exposed to the app as `PKC_BUILTIN_CHALLENGE_NAMES` in `src/lib/knownChallenges.ts`, so adding or removing a built-in in pkc-js propagates on next rebuild without any manual edits here. Anything not in that list — `captcha-canvas-v3`, `evm-contract-call`, `mintpass`, … — is an **external** challenge that the community operator must install on their node separately, e.g. `bitsocial challenge install @bitsocial/captcha-canvas-challenge`. The **Export CLI** button uses the same `PKC.challenges` list (via `isBuiltinChallenge`) to decide which names need a `challenge install` line prepended. ### Validation `CommunityChallengeSettingSchema` is imported directly from `@pkcprotocol/pkc-js` at runtime (via a Vite alias around its internal subpath) — no local mirror, so the schema cannot drift from upstream. See `src/pkc-schema.ts` and the `@pkc/*` aliases in `vite.config.ts`. ## Features - **Seedit-style editor**: add/remove/reorder challenges, edit name/path/options/description/pendingApproval, manage exclude rules (roles, post/reply counts, account age, rate limit, publication type, author addresses). - **Presets dropdown**: ships with `5chan board defaults`, `captcha only`, and `empty`. - **Paste JSONC** dialog, **file upload** (`.json` / `.jsonc`), and **download** as `challenges.jsonc`. - **Export CLI**: emits a `bitsocial challenge install …` + `bitsocial community edit …` shell script tailored to the current settings, flagging unknown challenge names with a TODO for the operator. - **Live JSON preview** with inline zod validation errors. - **LocalStorage draft** auto-saves every change. - **Share URL**: compresses the current settings into a URL fragment (`#s=…`). Browsers never send fragments to servers, so the blob stays client-side — but anyone holding the URL can decode it verbatim. A warning dialog makes this explicit before you copy. ## Security notes - The Share URL contains your full settings in lz-string-compressed but not encrypted form. Treat it like pasting the raw JSONC into a public place. - The `?raw` preset imports are bundled at build time. The runtime bundle performs no `fetch()` calls to third-party hosts. ## Architecture ``` src/ ├── pkc-schema.ts # re-exports CommunityChallengeSettingSchema via vite alias ├── types/challenges.ts # type-only re-exports from @pkcprotocol/pkc-js ├── state/ # useReducer store + localStorage/hash hydration ├── lib/ # jsonc, share (lz-string), cliExport, known-challenges catalog ├── presets/ # bundled .jsonc presets + registry └── components/ # Header, ChallengesEditor, ChallengeCard, ChallengeRow, # OptionsEditor, ExcludeRulesEditor, JsonPreview, # ImportDialog, ShareDialog, ExportCliDialog ``` ## TODO - **Scenario simulator.** For each challenge in the current settings, simulate which publications and author profiles would be challenged vs. excluded, across a library of mock authors (brand-new account, moderator, high-karma user, rate-limited user, wallet holder, banned author address, …). Should exercise every branch of `ChallengeExcludeSchema` — role / postCount / replyCount / postScore / replyScore / firstCommentTimestamp / rateLimit / rateLimitChallengeSuccess / publicationType / address / community / challenges. - Inline zod validation errors attached to the specific offending field (currently shown in the JSON preview panel only). - Drag-and-drop reordering for challenges (the reducer has `MOVE_CHALLENGE` and Move up/down buttons; drag handles are not yet wired up). ### Bitsocial Telegram Bots Repository: bitsocialnet/bitsocial-telegram-bots Source: https://github.com/bitsocialnet/bitsocial-telegram-bots#readme Description: Telegram feed bots for Bitsocial clients. Each bot monitors a specific client's community list and forwards new posts to Telegram channels/groups. # Bitsocial Telegram Bots Telegram feed bots for [Bitsocial](https://github.com/bitsocialnet) clients. Each bot monitors a specific client's community list and forwards new posts to Telegram channels/groups. ## Available Bots ### 5chan Feed Monitors all [5chan](https://github.com/bitsocialnet/5chan) directories (boards) from [`5chan-directories/`](https://github.com/bitsocialnet/lists/tree/master/5chan-directories) and posts new content to Telegram. Each post includes buttons to view it on 5chan and Seedit. ### Seedit Feed *(planned)* Will monitor [Seedit](https://github.com/bitsocialnet/seedit) communities and post new content to Telegram. ## Setup 1. **Clone the repository:** ```bash git clone https://github.com/bitsocialnet/bitsocial-telegram-bots.git cd bitsocial-telegram-bots ``` 2. **Install dependencies:** ```bash yarn install ``` 3. **Create a `.env` file** in the root directory: ```env # Required: Telegram Bot Token from @BotFather BOT_TOKEN=your_telegram_bot_token_here # Which bot to run (defaults to 5chan-feed) BOT_NAME=5chan-feed # Required: At least one destination must be set FEED_BOT_CHAT=-1001234567890 FEED_BOT_GROUP=-1001234567891 ``` 4. **Start the bot:** ```bash yarn start ``` Or run a specific bot: ```bash yarn start:5chan-feed ``` ## Environment Variables | Variable | Required | Description | |---|---|---| | `BOT_TOKEN` | Yes | Telegram bot token from [@BotFather](https://t.me/botfather) | | `BOT_NAME` | No | Which bot config to use (default: `5chan-feed`) | | `FEED_BOT_CHAT` | Yes* | Primary Telegram chat/channel ID | | `FEED_BOT_GROUP` | No | Secondary Telegram group ID | \* At least one of `FEED_BOT_CHAT` or `FEED_BOT_GROUP` must be set. ## How It Works 1. The bot fetches the community list for the configured client (e.g. `5chan-directories.json` for 5chan) 2. It cycles through each community, checking for new posts via Plebbit 3. New posts are formatted and sent to the configured Telegram destinations with inline buttons linking back to the client 4. Processed post CIDs are tracked in `history.json` to avoid duplicates 5. Cycles repeat every 30 seconds ## Adding a New Bot To add a bot for a different Bitsocial client: 1. Add a new config in `src/bot-configs.ts` with the client's list URL, community parser, and URL templates 2. Add a corresponding `start:` script in `package.json` 3. Deploy with a separate `.env` pointing to a different `BOT_TOKEN` and `BOT_NAME` ## License GPL-3.0-or-later ### bitsocial-github-alerts Repository: bitsocialnet/bitsocial-github-alerts Source: https://github.com/bitsocialnet/bitsocial-github-alerts#readme Description: A Telegram bot that posts compact GitHub notifications — pushes, releases, issues, pull requests, and more — to any chat, group, or forum topic. Run by the Bitsocial team for its repositories, but anyone can self host it. # bitsocial-github-alerts A Telegram bot that posts compact GitHub notifications — pushes, releases, issues, pull requests, and more — to any chat, group, or forum topic. Run by the [Bitsocial](https://github.com/bitsocialnet) team for its repositories, but anyone can self-host it. This is a fork of [mhkafadar/notifine](https://github.com/mhkafadar/notifine), stripped down to GitHub + Telegram only, with compact message formatting and added support for GitHub release notifications. All credit for the original architecture goes to the notifine authors. This repository carries no license of its own because upstream notifine has none; licensing follows upstream. ## Features - **Compact messages** — pushes show at most 5 commits (first line of each commit message, truncated to 72 chars) plus an "… and N more" line - **Release notifications** — one-liner when a release is published, with tag link, release name, pre-release marker, and the first line of the release notes - **Supported events**: push (incl. branch create/delete and force-push), release, issues, pull requests, comments (issue/PR review/commit), check runs, workflow runs, wiki edits, ping - **Branch filtering** — `?branch=` / `?exclude_branch=` glob patterns on the webhook URL - **Forum topics** — run `/start` inside a Telegram topic to receive notifications there - Built with Rust (actix-web + teloxide + diesel/Postgres) ## Usage 1. Open a chat with the bot (or add it to a group) and send `/start` 2. The bot replies with a webhook URL of the form `https://github.bitsocial.net/github/` 3. In your GitHub repo, open **Settings → Webhooks → Add webhook**, paste the URL, set content type to `application/json`, and pick the events you want ### Branch filtering ``` # Only main https://github.bitsocial.net/github/?branch=main # Multiple branches and wildcards https://github.bitsocial.net/github/?branch=main,release/* # Exclude noisy branches (exclusions take precedence) https://github.bitsocial.net/github/?exclude_branch=feature/*,dependabot/* ``` Applies to push, pull request, workflow run, and create/delete events. ## Self-hosting ### Environment variables | Variable | Required | Description | | --- | --- | --- | | `DATABASE_URL` | yes | Postgres connection string | | `WEBHOOK_BASE_URL` | yes | Public base URL GitHub uses to reach the server, e.g. `https://github.bitsocial.net` | | `GITHUB_TELOXIDE_TOKEN` | yes | Telegram bot token from [@BotFather](https://t.me/BotFather) | | `PORT` | no | HTTP listen port (default `8080`) | | `ADMIN_LOGS` | no | `ACTIVE` to send admin logs to Telegram (default `NOT_ACTIVE`) | | `TELEGRAM_ADMIN_CHAT_ID` | no | Chat id that receives admin logs | | `ADMIN_LOG_LEVEL` | no | 0–255 verbosity threshold for admin logs (default `50`) | Database migrations are embedded and run automatically at startup. ### Docker Compose (production) A production compose file is provided in [`deploy/docker-compose.yml`](https://github.com/bitsocialnet/bitsocial-github-alerts/blob/HEAD/deploy/docker-compose.yml). It runs the prebuilt image `ghcr.io/bitsocialnet/bitsocial-github-alerts:latest` next to a Postgres 17 container and binds the app to `127.0.0.1:8090` (put a reverse proxy such as Caddy or nginx in front). ```bash mkdir bitsocial-github-alerts && cd bitsocial-github-alerts curl -fsSLO https://raw.githubusercontent.com/bitsocialnet/bitsocial-github-alerts/main/deploy/docker-compose.yml cat > .env <<'ENV' GITHUB_TELOXIDE_TOKEN= WEBHOOK_BASE_URL=https://github.example.com DATABASE_PASSWORD= ENV docker compose up -d ``` `DATABASE_URL` is derived from `DATABASE_PASSWORD` inside the compose file; the other variables come from `.env`. ### Local development ```bash cp .env.example .env # fill in values docker compose up -d bitsocial-github-alerts-db cargo run ``` ## Docker image Images are published to GHCR on every push to `main` and on version tags: ``` ghcr.io/bitsocialnet/bitsocial-github-alerts:latest ``` ## Credits Forked from [mhkafadar/notifine](https://github.com/mhkafadar/notifine). Report bugs for this fork by [creating an issue](https://github.com/bitsocialnet/bitsocial-github-alerts/issues/new). ### 5chan Board Manager Repository: bitsocialnet/5chan-board-manager Source: https://github.com/bitsocialnet/5chan-board-manager#readme Description: A CLI tool that implements 4chan style thread auto archiving and purging for 5chan boards. # 5chan Board Manager A CLI tool that implements 4chan-style thread auto-archiving and purging for 5chan boards. ### Feature 1: Thread limit / auto-archive - After each board update, determine thread positions in active sort - Filter out pinned threads (they're exempt) - Count non-pinned threads; any beyond position `per_page × pages` → archive via `createCommentModeration({ commentModeration: { archived: true } })` - Archived threads are read-only (pkc-js already enforces this) ### Feature 2: Bump limit - Track reply counts for active threads - When a thread reaches `bump_limit` replies → archive it via `createCommentModeration({ commentModeration: { archived: true } })` ### Feature 3: Delayed purge - Track when threads were archived - After `archive_purge_seconds` has elapsed since archiving → purge via `createCommentModeration({ commentModeration: { purged: true } })` - Purging is time-based rather than activity-based, so it also runs on a periodic sweep (`PURGE_SWEEP_INTERVAL_SECONDS`, default 60s) instead of only when a `community.update` event arrives. Without the sweep, a board that went quiet after archiving would keep expired threads in the archive until unrelated activity woke the manager. ### Feature 4: Author-deleted comment purging - On each update, scan comments and replies for `deleted === true` - Purge via `createCommentModeration({ commentModeration: { purged: true } })` - Duplicate purge moderations (if the board hasn't processed prior purge yet) are harmless no-ops ### Config Directory Layout Config and state are stored per-board under `~/.config/5chan/`, managed via `5chan board add/edit/remove` or manual editing: ``` ~/.config/5chan/ ├── global.json # shared settings (rpcUrl, defaults) └── boards/ ├── random.bso/ │ ├── config.json # { "address": "random.bso" } │ ├── state.json # auto-created (signers, archivedThreads) │ └── state.json.lock # transient lock file ├── tech.bso/ │ └── config.json # { "address": "tech.bso", "bumpLimit": 500 } ├── flash.bso/ │ └── config.json # { "address": "flash.bso", "perPage": 30, "pages": 1 } └── custom.bso/ └── config.json # { "address": "custom.bso", "moderationReasons": { ... } } ``` **global.json** (optional): ```json { "rpcUrl": "ws://localhost:9138", "defaults": { "perPage": 15, "pages": 10, "bumpLimit": 300, "archivePurgeSeconds": 172800, "moderationReasons": { "archiveCapacity": "5chan board manager: thread archived — exceeded board capacity", "archiveBumpLimit": "5chan board manager: thread archived — reached bump limit", "purgeArchived": "5chan board manager: thread purged — archive retention expired", "purgeDeleted": "5chan board manager: content purged — author-deleted" } } } ``` **Minimal config:** a single directory `boards/my-board.bso/config.json` containing `{ "address": "my-board.bso" }` All fields except each board's `address` are optional: - `rpcUrl` — falls back to `PKC_RPC_WS_URL` env var, then `ws://localhost:9138` - `defaults` — applied to all boards unless overridden per-board - Per-board fields (`perPage`, `pages`, `bumpLimit`, `archivePurgeSeconds`, `moderationReasons`) override `defaults` - `moderationReasons` — optional object with `archiveCapacity`, `archiveBumpLimit`, `purgeArchived`, `purgeDeleted` string fields. Per-board values override defaults per-field (not the whole object). These reason strings are passed to `createCommentModeration()` so PKC clients can display why a thread was archived or purged. - Board directory names must match the address field in `config.json` ## Installation Docker is the only supported install path. The published image (`ghcr.io/bitsocialnet/5chan-board-manager:latest`) is built and tested with the bitsocial-cli version the default preset assumes, including the `@bitsocial/spam-blocker-challenge` that the preset references. Running 5chan outside Docker — or against a bitsocial-cli you manage yourself — is possible but unsupported; you will have to keep the challenge and auth-key wiring in sync by hand (see [Standalone](#standalone-bitsocial-cli-already-running) below). ### Docker Compose (recommended) #### Full stack (with bitsocial-cli) If you **don't** already have [bitsocial-cli](https://github.com/bitsocialnet/bitsocial-cli) running, use the full stack compose file which boots both bitsocial-cli (PKC RPC server) and 5chan together.: ```bash wget -O docker-compose.yml https://raw.githubusercontent.com/bitsocialnet/5chan-board-manager/master/docker-compose.example.yml docker compose up -d # Install the spam-blocker challenge referenced by the default preset # (skip this only if you also remove spam-blocker entries from your preset # or always use --skip-apply-defaults): docker compose exec bitsocial bitsocial challenge install @bitsocial/spam-blocker-challenge # Optional: install this before enabling the wordfilter preset example docker compose exec bitsocial bitsocial challenge install @bitsocial/wordfilter-challenge@0.3.0 # Now add boards via 5chan board add (see Config Directory Layout above) ``` See [`docker-compose.example.yml`](https://github.com/bitsocialnet/5chan-board-manager/blob/HEAD/docker-compose.example.yml) for the full configuration. #### Quick usage (Docker, full stack) Use this flow to create a new board with `bitsocial-cli` and immediately add it to 5chan. > **Note:** The compose file ships with a pre-configured RPC auth key so both containers can connect out of the box. For production, replace it with your own random string: > ```bash > sed -i "s/TFCh0joRU60KwlfVaprP2uenw7NCdAwsBCF5UDoVg/$(openssl rand -base64 32 | tr -d '/+=')/g" docker-compose.yml > ``` > **Note:** The container starts gracefully even with no boards configured — it waits for boards to be added and picks them up automatically via config hot-reload. ```bash wget -O docker-compose.yml https://raw.githubusercontent.com/bitsocialnet/5chan-board-manager/master/docker-compose.example.yml docker compose up -d # Install the spam-blocker challenge referenced by the default preset docker compose exec bitsocial bitsocial challenge install @bitsocial/spam-blocker-challenge # Optional: install this before enabling the wordfilter preset example docker compose exec bitsocial bitsocial challenge install @bitsocial/wordfilter-challenge@0.3.0 # Create a community (copy the created address from output) docker compose exec bitsocial bitsocial community create \ --title "My Board title" \ --description "My Board description" # Add the created community to 5chan board manager docker compose exec -it 5chan 5chan board add # Verify it was added docker compose exec 5chan 5chan board list # You can load the board now by its address in 5chan or any other web ui ``` #### Standalone (bitsocial-cli already running) If you **already** have bitsocial-cli running separately (on the host, in another compose stack, etc.), use the standalone compose file which only runs 5chan: ```bash cp docker-compose.standalone.example.yml docker-compose.yml # Edit PKC_RPC_WS_URL in docker-compose.yml — replace YOUR-AUTH-KEY with # your bitsocial-cli auth key (find it with: docker logs 2>&1 | grep "secret auth key") docker compose up -d ``` Set `PKC_RPC_WS_URL` to the address of your existing instance, **including the auth key** as a path segment: - **bitsocial-cli on the host (no container):** Use `ws://host.docker.internal:9138/YOUR-AUTH-KEY`. The example compose file includes `extra_hosts: ["host.docker.internal:host-gateway"]` so this works on Linux, macOS, and Windows. - **bitsocial-cli in another Docker container/network:** Use the container or service name, e.g. `ws://bitsocial:9138/YOUR-AUTH-KEY`, and make sure both containers share the same Docker network. **You must install the spam-blocker challenge on your bitsocial-cli instance**, because the default preset (`src/presets/community-defaults.jsonc`) references it and `5chan board add --apply-defaults` will be rejected by the PKC RPC server if the challenge is missing: ```bash bitsocial challenge install @bitsocial/spam-blocker-challenge ``` If you do not want this dependency, remove both `@bitsocial/spam-blocker-challenge` entries from your preset (they are marked optional in the file) or use `--skip-apply-defaults`. The bundled preset also contains a disabled wordfilter example. If you enable it, install the challenge on the same bitsocial-cli instance first: ```bash bitsocial challenge install @bitsocial/wordfilter-challenge@0.3.0 ``` See [`docker-compose.standalone.example.yml`](https://github.com/bitsocialnet/5chan-board-manager/blob/HEAD/docker-compose.standalone.example.yml) for the configuration. ### Running commands inside Docker Use `docker compose exec` to run additional `5chan` CLI commands inside the running container: ```bash # Add a board docker compose exec 5chan 5chan board add random.bso # List configured boards docker compose exec 5chan 5chan board list # Edit a board's config docker compose exec 5chan 5chan board edit random.bso --bump-limit 500 # Open a board's config in $EDITOR for interactive editing docker compose exec 5chan 5chan board edit random.bso -i # Reset a board field to global default docker compose exec 5chan 5chan board edit random.bso --reset per-page # Reset moderation reasons to defaults docker compose exec 5chan 5chan board edit random.bso --reset moderation-reasons # Set global defaults for all boards docker compose exec 5chan 5chan defaults set --per-page 20 --bump-limit 500 # Open global defaults in $EDITOR for interactive editing docker compose exec 5chan 5chan defaults set -i # Remove a board docker compose exec 5chan 5chan board remove random.bso ``` The container auto-reloads when files in the config directory change, so boards added/edited via `5chan board add/edit` take effect immediately without restarting. ### Build locally ```bash docker build -t 5chan-board-manager . docker run -d -v /path/to/data:/data 5chan-board-manager ``` ### Data paths | Container path | Description | |---|---| | `/data/5chan/global.json` | Global config (optional — rpcUrl, defaults) | | `/data/5chan/boards/
/config.json` | Per-board config (created by `5chan board add`) | | `/data/5chan/boards/
/state.json` | Per-board state (auto-created: signers, archived threads) | ### Board creation and defaults preset If you are not using the Docker quick usage flow above, create the board with `bitsocial community create` first, then add it to 5chan with `5chan board add`. > **Media rendering:** Keep `settings.fetchThumbnailUrls` enabled for 5chan boards. Bitsocial publications store media links, not uploaded media files, so clients need the thumbnail metadata fetched by the community node to know image and video dimensions before rendering. Without it, clients must fall back to generic containers that can size media poorly or cause layout shifts. > > The bundled preset enables this setting for new boards. By default, the community node fetches media links directly, which exposes the node's IP address to those hosts. Operators who need to hide the node's IP should also configure `settings.fetchThumbnailUrlsProxyUrl`. Run `5chan board add --help` for full details on preset defaults flags (`--apply-defaults`, `--skip-apply-defaults`, `--interactive-apply-defaults`). In interactive terminals, defaults are shown with an `[A]ccept / [M]odify / [S]kip` prompt; choosing Modify opens the preset in `$EDITOR` as an annotated JSONC file with `//` comments explaining each field. Preset JSONC is validated with Zod. Both plain JSON and JSONC (with `//` comments) are accepted as preset files. `boardSettings` must follow pkc-js `CommunityEditOptions`: https://github.com/pkcprotocol/pkc-js?tab=readme-ov-file#communityeditcommunityeditoptions Bundled preset JSONC defaults: [`src/presets/community-defaults.jsonc`](https://github.com/bitsocialnet/5chan-board-manager/blob/HEAD/src/presets/community-defaults.jsonc) `boardSettings` is merged into `community.edit()` with "missing only" semantics (only absent values are applied). `boardManagerSettings` is used as default values for `board add` config fields, and explicit CLI flags override these defaults. The bundled preset file is `src/presets/community-defaults.jsonc`. ### Optional wordfilters Wordfilters replace configured text in a publishing client before the publication is signed. The community node then rejects any publication that still contains a filtered source word. Consequently, enable wordfilters only after the clients used by the board support the `wordfilter/v1` contract. An older client cannot publish the unfiltered text; it receives the configured challenge error instead. The bundled preset contains a commented example with 4chan's classic filters, `soy` -> `onions`, `tbh` -> `desu` and `smh` -> `baka`, applied to post and reply content, comment-edit content, and post titles. To enable it for a new board: 1. Install `@bitsocial/wordfilter-challenge@0.3.0` on the bitsocial-cli instance that hosts the community. 2. Run `5chan board add ADDRESS --interactive-apply-defaults`. 3. Choose **Modify**, uncomment the wordfilter challenge object, review the rule, then save the preset. The active challenge object has this shape when configuring a custom preset or editing an existing community's `settings.challenges` array: ```json { "name": "@bitsocial/wordfilter-challenge", "description": "Replaces configured words before publications are signed.", "options": { "wordfilter/v1/rules": "[{\"src\":\"soy\",\"dst\":\"onions\"},{\"src\":\"tbh\",\"dst\":\"desu\"},{\"src\":\"smh\",\"dst\":\"baka\"}]", "wordfilter/v1/fieldNames": "[\"comment.content\",\"comment.title\",\"commentEdit.content\"]", "error": "This board replaces certain words. Please retry after refreshing the board." }, "publicOptions": [ "wordfilter/v1/rules", "wordfilter/v1/fieldNames", "error" ] } ``` The two contract options must remain in `publicOptions`: publishing clients need them to produce text the community will accept. Field paths start with the publication type (`comment.content`, `commentEdit.content`, `vote.author.displayName`, ...), as required by wordfilter-challenge 0.3.0; bare paths such as `content` are rejected when the configuration is saved, and clients written against the 0.2.0 bare-path format do not apply prefixed paths, so their posts are rejected until they are updated. Rules are literal and case-insensitive substring matches, not regular expressions and not whole-word matches. This example matches `soy`, `Soy`, and `SOY`, and also the `soy` inside `soybean` (which becomes `onionsbean`, exactly as on 4chan), but not `s o y` or Unicode lookalikes. Invalid rules within one wordfilter challenge are rejected when the community configuration is saved. Each challenge allows at most 64 rules; source and destination strings are limited to 128 characters; and replacements that could loop or reintroduce another source in that challenge are rejected. Conflicts between separate wordfilter challenges cannot be validated in isolation; they surface during client application or as a publication rejection. See the [`@bitsocial/wordfilter-challenge` contract](https://github.com/bitsocialnet/wordfilter-challenge#the-wordfilterv1-contract) for the complete validation and client behavior. ## Commands * [`5chan board add ADDRESS`](#5chan-board-add-address) * [`5chan board edit ADDRESS`](#5chan-board-edit-address) * [`5chan board list`](#5chan-board-list) * [`5chan board remove ADDRESS`](#5chan-board-remove-address) * [`5chan defaults set`](#5chan-defaults-set) * [`5chan help [COMMAND]`](#5chan-help-command) * [`5chan logs`](#5chan-logs) * [`5chan start`](#5chan-start) ## `5chan board add ADDRESS` Add one or more boards to the config ``` USAGE $ 5chan board add ADDRESS... [--rpc-url ] [--per-page ] [--pages ] [--bump-limit ] [--archive-purge-seconds ] [--apply-defaults] [--skip-apply-defaults] [--interactive-apply-defaults] [--defaults-preset ] ARGUMENTS ADDRESS... Board address(es) to add (one or more, space-separated) FLAGS --apply-defaults Apply preset defaults silently (no prompts) --archive-purge-seconds= Seconds after archiving before purge --bump-limit= Bump limit for threads --defaults-preset= Path to a custom preset JSON file --interactive-apply-defaults Interactively review and modify preset defaults before applying --pages= Number of pages --per-page= Posts per page --rpc-url= [default: ws://localhost:9138, env: PKC_RPC_WS_URL] PKC RPC WebSocket URL (for validation) --skip-apply-defaults Skip applying preset defaults DESCRIPTION Add one or more boards to the config Multiple addresses may be supplied space-separated; the same defaults decision and preset are applied to each. All addresses are validated and checked for conflicts up front, so nothing is written if any address is invalid or already present. Preset defaults behavior: --apply-defaults Apply all preset defaults silently (no prompts) --skip-apply-defaults Skip preset defaults silently --interactive-apply-defaults Review defaults, accept all, modify in $EDITOR, or skip (requires TTY) Interactive TTY (no flags) Same as --interactive-apply-defaults: shows [A]ccept / [M]odify / [S]kip Non-interactive (no flags) Errors; requires --apply-defaults or --skip-apply-defaults When choosing [M]odify, the preset opens in your editor ($VISUAL > $EDITOR > vi/notepad). Modified presets are validated before applying; invalid changes fail the command. Note: "board add" only accepts 5chan settings flags (pagination, bump limits, archiving). To set board settings (title, description, rules, etc.), use a WebUI or bitsocial-cli: https://github.com/bitsocialnet/bitsocial-cli#bitsocial-community-edit-address EXAMPLES $ 5chan board add random.bso $ 5chan board add random.bso tech.bso flash.bso $ 5chan board add tech.bso --bump-limit 500 $ 5chan board add flash.bso --per-page 30 --pages 1 $ 5chan board add my-board.bso --rpc-url ws://custom-host:9138 $ 5chan board add my-board.bso --apply-defaults $ 5chan board add my-board.bso other-board.bso --apply-defaults $ 5chan board add my-board.bso --skip-apply-defaults $ 5chan board add my-board.bso --interactive-apply-defaults $ 5chan board add my-board.bso --apply-defaults --defaults-preset ./my-preset.json ``` _See code: [src/commands/board/add.ts](https://github.com/bitsocialnet/5chan-board-manager/blob/v0.2.27/src/commands/board/add.ts)_ ## `5chan board edit ADDRESS` Edit 5chan settings for an existing board ``` USAGE $ 5chan board edit ADDRESS [-i | --per-page | --pages | --bump-limit | --archive-purge-seconds | --reset ] ARGUMENTS ADDRESS Board address to edit FLAGS -i, --interactive Open the board config in $EDITOR for interactive editing --archive-purge-seconds= Seconds after archiving before purge --bump-limit= Bump limit for threads --pages= Number of pages --per-page= Posts per page --reset= Comma-separated fields to reset to defaults (per-page, pages, bump-limit, archive-purge-seconds, moderation-reasons) DESCRIPTION Edit 5chan settings for an existing board This command configures how 5chan manages the board (pagination, bump limits, archiving). Use --interactive (-i) to open the board config in $EDITOR for direct viewing/editing. To edit board settings (title, description, rules, etc.), use a WebUI or bitsocial-cli: https://github.com/bitsocialnet/bitsocial-cli#bitsocial-community-edit-address EXAMPLES $ 5chan board edit tech.bso --bump-limit 500 $ 5chan board edit flash.bso --per-page 30 --pages 1 $ 5chan board edit random.bso --reset per-page,bump-limit $ 5chan board edit random.bso --per-page 20 --reset bump-limit $ 5chan board edit random.bso --reset moderation-reasons $ 5chan board edit random.bso --interactive $ 5chan board edit random.bso -i ``` _See code: [src/commands/board/edit.ts](https://github.com/bitsocialnet/5chan-board-manager/blob/v0.2.27/src/commands/board/edit.ts)_ ## `5chan board list` List all board addresses ``` USAGE $ 5chan board list DESCRIPTION List all board addresses EXAMPLES $ 5chan board list ``` _See code: [src/commands/board/list.ts](https://github.com/bitsocialnet/5chan-board-manager/blob/v0.2.27/src/commands/board/list.ts)_ ## `5chan board remove ADDRESS` Remove a board from the config ``` USAGE $ 5chan board remove ADDRESS ARGUMENTS ADDRESS Community address to remove DESCRIPTION Remove a board from the config EXAMPLES $ 5chan board remove random.bso ``` _See code: [src/commands/board/remove.ts](https://github.com/bitsocialnet/5chan-board-manager/blob/v0.2.27/src/commands/board/remove.ts)_ ## `5chan defaults set` Set global default settings for all boards ``` USAGE $ 5chan defaults set [-i | --per-page | --pages | --bump-limit | --archive-purge-seconds | --reset ] FLAGS -i, --interactive Open defaults in $EDITOR for interactive editing --archive-purge-seconds= Seconds after archiving before purge --bump-limit= Bump limit for threads --pages= Number of pages --per-page= Posts per page --reset= Comma-separated fields to remove from defaults (per-page, pages, bump-limit, archive-purge-seconds, moderation-reasons) DESCRIPTION Set global default settings for all boards Defaults in global.json apply to every board unless overridden per-board. Use --interactive (-i) to open the defaults object in $EDITOR for direct editing. EXAMPLES $ 5chan defaults set --per-page 20 $ 5chan defaults set --bump-limit 500 --pages 10 $ 5chan defaults set --reset per-page,bump-limit $ 5chan defaults set --per-page 20 --reset bump-limit $ 5chan defaults set --interactive $ 5chan defaults set -i ``` _See code: [src/commands/defaults/set.ts](https://github.com/bitsocialnet/5chan-board-manager/blob/v0.2.27/src/commands/defaults/set.ts)_ ## `5chan help [COMMAND]` Display help for 5chan. ``` USAGE $ 5chan help [COMMAND...] [-n] ARGUMENTS [COMMAND...] Command to show help for. FLAGS -n, --nested-commands Include all nested commands in the output. DESCRIPTION Display help for 5chan. ``` ## `5chan logs` View the latest 5chan daemon log file. By default dumps the full log and exits. Use --follow to stream new output in real-time (like tail -f). ``` USAGE $ 5chan logs [-f] [-n ] [--since ] [--until ] [--logPath ] [--stdout | --stderr] FLAGS -f, --follow Follow log output in real-time (like tail -f) -n, --tail= [default: all] Number of log entries to show from the end. Use "all" to show everything. --logPath= Specify the directory containing log files --since= Show logs since timestamp (ISO 8601, e.g. 2026-01-02T13:23:37Z) or relative time (e.g. 30s, 42m, 2h, 1d) --stderr Show only stderr log entries (output of pkc-logger library) --stdout Show only stdout log entries --until= Show logs before timestamp (ISO 8601, e.g. 2026-01-02T13:23:37Z) or relative time (e.g. 30s, 42m, 2h, 1d) DESCRIPTION View the latest 5chan daemon log file. By default dumps the full log and exits. Use --follow to stream new output in real-time (like tail -f). EXAMPLES $ 5chan logs $ 5chan logs -f $ 5chan logs -n 50 $ 5chan logs --since 5m $ 5chan logs --since 2026-01-02T13:23:37Z --until 2026-01-02T14:00:00Z $ 5chan logs --since 1h -f $ 5chan logs --stdout $ 5chan logs --stderr $ 5chan logs --stdout -f ``` _See code: [src/commands/logs.ts](https://github.com/bitsocialnet/5chan-board-manager/blob/v0.2.27/src/commands/logs.ts)_ ## `5chan start` Start board managers for all configured boards ``` USAGE $ 5chan start [-c ] [--log-path ] FLAGS -c, --config-dir= Path to config directory (overrides default) --log-path= [default: /home/runner/.local/state/5chan] Directory to store daemon log files DESCRIPTION Start board managers for all configured boards Board managers enforce imageboard-style thread lifecycle rules on each board: - Archive threads that exceed board capacity (perPage × pages) - Archive threads that reach the bump limit - Purge archived threads after the retention period expires - Purge author-deleted threads and replies The config directory is watched for changes; boards are hot-reloaded (added, removed, or restarted) without requiring a full restart. Daemon output is written to a rotated log file (default: /home/runner/.local/state/5chan). View it with `5chan logs`. stderr is suppressed on the terminal; real uncaught errors still reach the terminal. EXAMPLES $ 5chan start $ 5chan start --config-dir /path/to/config $ 5chan start --log-path /var/log/5chan ``` _See code: [src/commands/start.ts](https://github.com/bitsocialnet/5chan-board-manager/blob/v0.2.27/src/commands/start.ts)_ ## Config Hot-Reload `5chan start` watches the config directory (`boards/` and `global.json`) using chokidar with a 200ms debounce, plus a periodic reconcile (every 2s) that guarantees changes are picked up even if the OS watcher drops events (e.g. inotify queue overflow). Changes that arrive while a reload is already running are queued and applied right after it. When any config file changes: 1. Loads and validates the new config 2. Diffs old vs new boards 3. Stops board managers for removed boards 4. Restarts board managers for boards with changed config 5. Starts board managers for added boards 6. Logs the delta: `config reloaded: +N added, -N removed, ~N changed, M running` This means you can add, edit, or remove boards while the board manager is running — either by editing config files directly or by running `5chan board add/edit/remove` in another terminal. When global config changes (rpcUrl, defaults), all running boards are restarted. ## File Locking Each board manager acquires a PID-based lock file (`{statePath}.lock`) to prevent concurrent board managers on the same board. On startup: 1. Attempts to create lock file exclusively (`wx` flag) 2. If lock exists, reads the PID and checks if it's still alive (`process.kill(pid, 0)`) 3. If alive — throws `Another board manager (PID N) is already running` 4. If stale — removes the old lock and retries The lock is released when the board manager stops. ## Purge Sweep Archiving is driven by `community.update` events, because it depends on board activity (new threads pushing old ones past capacity, replies hitting the bump limit). Purging is different: once a thread is archived, only the clock decides when its retention expires. So the board manager also runs a periodic sweep that purges archived threads whose `archivePurgeSeconds` has elapsed, independent of update events. A board that goes quiet right after archiving would otherwise keep expired threads in the archive indefinitely, since nothing would trigger the update handler. | Env var | Default | Purpose | |---|---|---| | `PURGE_SWEEP_INTERVAL_SECONDS` | `60` | How often to check for archived threads past their retention window. Set to `0` to disable the sweep and fall back to purging on update events only. | The sweep does no network fetching — it reads the archived-thread timestamps already held in the state file, and only publishes when something is genuinely due. It is serialized against the update handler, so a thread is never purged twice concurrently. ## Author-Deleted Comment Purging The board manager detects comments and replies that were deleted by their author (where `comment.deleted === true`) and purges them via `createCommentModeration({ commentModeration: { purged: true } })`. Once purged, the comment is removed from the board and won't appear in future listings. If a purge hasn't been processed yet, the next cycle may re-publish a redundant purge moderation, which is a harmless no-op. ## Auto Mod Signer Management On startup for each board (using the internally-created PKC instance): 1. Check state JSON for a signer private key for this board address 2. If none exists, create one via `pkc.createSigner()` and save to state JSON 3. Check board roles via `community.roles` for the signer's address 4. If not a mod, auto-add via `community.edit()` (works because we run on `LocalCommunity` or `RpcLocalCommunity` — we own the board) Logged via `pkc-logger` when creating signer or adding mod role. ## Address Change Handling When bitsocial-cli changes a board's address (e.g., from a hash like `12D3KooW...` to a named address like `random.bso`), the board manager detects the change automatically via the pkc-js `update` event and migrates all associated files: 1. Signer key is moved to the new address in the state file 2. Board directory is renamed from `boards/{oldAddress}/` to `boards/{newAddress}/` (with updated `address` field in `config.json`) 3. Lock file is re-acquired for the new address 4. Internal maps are updated so moderation continues uninterrupted The mod signer carries over to the new address — no need to re-assign the moderator role. If the migration fails (e.g., lock conflict on the new address), the board manager logs the error and continues operating under the old address. ## State Persistence State is stored as `state.json` inside each board's directory (`boards/{address}/state.json`), alongside the board's `config.json`. ```json { "signers": { "": { "privateKey": "..." } }, "archivedThreads": { "": { "archivedTimestamp": 1234567890 } } } ``` - **`signers`**: maps board address → mod signer private key (auto-created if missing) - **`archivedThreads`**: maps comment CID → archive metadata (entries removed on purge) - State writes use atomic temp-then-rename to prevent corruption - Loaded on startup, written on archive, entries removed on purge ## Cold Start The script may start long after the board has been running. On first run, many threads may need locking/purging at once. No rate limiting is needed — same-node publishing has no pubsub overhead. The first cycle may be heavier; steady-state handles a few threads per update. ## Idempotency Before archiving, checks the thread's `archived` property. Skips if already archived (pkc-js throws on duplicate moderation actions). ## Logging Uses `pkc-logger` (same logger as the pkc-js ecosystem). Key events logged: - Board manager start/stop - Threads archived (with CID and reason: capacity vs bump limit) - Threads purged - Author-deleted comments purged - Config hot-reload events - Mod role auto-added - Errors **Daemon log file.** When `5chan start` runs, stdout and stderr are captured to a rotated log file under `$XDG_STATE_HOME/5chan/log/` (inside Docker: `/data/5chan/log/`). Each entry is prefixed with `[ISO-timestamp] [stdout|stderr]`. At most 5 files are retained; the oldest is deleted on each restart. Each file is capped at 20MB. stderr is suppressed on the terminal — only real uncaught errors reach it. Use `5chan logs` (or `5chan logs -f`) to view daemon output: ```bash docker compose exec 5chan 5chan logs # dump latest log docker compose exec 5chan 5chan logs -f # stream live docker compose exec 5chan 5chan logs --stderr -f # only pkc-logger output ``` **Docker:** debug logging is **off** by default. The example compose files set `DEBUG=bitsocial:5chan-board-manager*` on the `5chan` service so daemon output flows into the log file. Because stderr is no longer teed to the terminal, `docker logs` will be quieter than before — use `5chan logs` to see the full debug stream. To silence the log file, drop the variable from your compose file (or override it to empty): ```yaml environment: DEBUG: "" ``` To broaden the captured namespaces (e.g. to include pkc-js events), widen `DEBUG`: ```yaml environment: DEBUG: "bitsocial:5chan-board-manager*,pkc*,pkc-js*" ``` ## Heartbeat & Health Check Each board manager runs a periodic heartbeat that: - Logs a line per tick at trace level (`[board ] heartbeat — last update: (s ago)`) so you can confirm the daemon is alive even when a board is idle. Visible with the default `DEBUG='bitsocial:5chan-board-manager*'`. - Touches a shared heartbeat file (default `$XDG_STATE_HOME/5chan/heartbeat`, i.e. `/data/5chan/heartbeat` in Docker) for the Docker healthcheck to consume. - If `now - lastUpdateAt` exceeds `HEARTBEAT_STALE_UPDATE_SECONDS`, an error-level `[board ] no update events for s` line is logged each tick. The process is **not** restarted automatically — operators should monitor the log or rely on the file-mtime healthcheck below. | Env var | Default | Purpose | |---|---|---| | `HEARTBEAT_INTERVAL_SECONDS` | `300` | Tick cadence. | | `HEARTBEAT_STALE_UPDATE_SECONDS` | `1800` | If no `community.update` event has fired within this window, a stale-update warning is logged each tick. | | `HEARTBEAT_FILE` | `/heartbeat` | Override the heartbeat file path (e.g. for tests). | The example compose files include a healthcheck that reports `unhealthy` if the heartbeat file's mtime is older than 10 minutes — useful for `docker ps` and external watchdogs. Note this only catches "process completely dead" (the file's mtime stops advancing only when the interval timer stops firing); if the RPC connection is stuck but the process is alive, the heartbeat file keeps getting touched, so watch the log for the stale-update warning instead. ## 4chan Board Behavior Reference ### Board capacity Total thread capacity = `per_page × pages`. Both are **configurable per board**. | Setting | Range | Description | |---------|-------|-------------| | `per_page` | 15–30 | Threads per index page (e.g., /b/ = 15, /v/ = 20, /f/ = 30) | | `pages` | 1–10 | Number of index pages (e.g., /f/ = 1, most boards = 10) | Capacity examples: /b/ = 150, /v/ = 200, /f/ = 30. ### Thread lifecycle 1. **New thread created** → sits at top of page 1 2. **Bumped by replies** → moves back to top of page 1 3. **Sinks gradually** as newer threads get replies 4. **Falls off last page** → archived (read-only, ~48h) 5. **Purged** → permanently deleted from 4chan's servers ### Bumping A reply moves the thread to the top of page 1. This is equivalent to PKC's **"active sort"**, which orders threads by `lastReplyTimestamp`. ### Bump limit Configurable per board (300–500+). After N replies, new replies no longer bump the thread, but it still accepts replies until it falls off the last page. Examples: /b/ = 300, /3/ = 310, /v/ = 500. ### Pinned (sticky) threads Sit at top of page 1, exempt from thread limit and archiving. "Pinned" and "sticky" are the same thing. ### Archive vs purge - **Archived** = locked/read-only, still visible for ~48 hours - **Purged** = permanently deleted from 4chan's servers - Not all boards have archives (`is_archived` flag in API) ### Third-party archives External services (archive.4plebs.org, desuarchive.org) independently scrape and preserve threads before purge. ### Other per-board settings from 4chan API `image_limit`, `max_filesize`, `max_comment_chars`, `cooldowns`, `spoilers`, `country_flags`, `user_ids`, `forced_anon`, etc. ## Testing Unit tests run under Vitest: ```bash npm test ``` End-to-end tests run the real board manager against a real PKC RPC server: ```bash npm run test:e2e ``` The e2e suite spawns its own Kubo (IPFS) daemon on random ports and an in-process PKC RPC server on port `19138`. All state is written to temp directories that are removed at teardown. No `bitsocial-cli` or docker-compose stack needs to be running — Kubo is provided by the `kubo` npm package installed as a devDependency. To point the suite at an externally-running RPC instead (e.g. a long-lived docker-compose stack), set `PKC_RPC_WS_URL` before invoking the script — when set, global-setup skips spawning and uses the provided URL verbatim: ```bash PKC_RPC_WS_URL=ws://localhost:9138/YOUR-AUTH-KEY npm run test:e2e ``` ## Differences from 4chan | Behavior | 4chan | This module | |----------|------|-------------| | **Bump limit** | Threads past bump limit still accept replies — they just stop rising in the catalog | Threads are **archived** (no more replies) because pkc-js has no "stop bumping without archiving" mechanism | | **Sage** | Replying with `sage` in the email field prevents the thread from being bumped | Not supported — PKC has no equivalent mechanism, so all replies bump the thread | | **Image limit** | Per-thread image limit (e.g., 150 images on /b/) after which no more images can be posted | Not implemented — pkc-js has its own file-size constraints but no per-thread image count limit | ## PKC-js Implementation ### Architecture External module using pkc-js's public API: - No pkc-js core modifications needed - Uses `pkc.createCommentModeration()` for both archiving and purging - Listens to board `update` events (via `community.on('update', ...)`) to detect new posts - Gets thread positions from board post feeds (`community.posts.pageCids.active`), or falls back to sorting preloaded pages by `lastReplyTimestamp` descending (then `postNumber`) ### Configurable settings Uses 4chan field names for interoperability. | Setting | Default | 4chan range | Description | |---------|---------|-------------|-------------| | `per_page` | 15 | 15–30 | Threads per index page | | `pages` | 10 | 1–10 | Number of index pages | | `bump_limit` | 300 | 300–500 | Max replies before thread is archived | | `archive_purge_seconds` | 172800 (48h) | ~48h | Seconds before archived posts are purged (no 4chan equivalent, 4chan uses ~48h) | | `moderationReasons` | (see below) | — | Reason strings passed to `createCommentModeration()`. Fields: `archiveCapacity`, `archiveBumpLimit`, `purgeArchived`, `purgeDeleted` | **Max active threads** = `per_page × pages` (default: 150) ### API note Cannot do `community.posts.getPage("active")`. Must either: 1. Use `community.posts.pageCids.active` to get the CID, then fetch that page 2. Or fall back to sorting preloaded pages by `lastReplyTimestamp` descending, then `postNumber` (approximates active sort without needing `pageCids.active`) ### Board record size constraint The entire board IPFS record is capped at 1MB (`MAX_FILE_SIZE_BYTES_FOR_COMMUNITY_IPFS`). `community.posts.pages.hot` is preloaded into the record with whatever space remains after the rest of the record (title, description, roles, challenges, etc.). - If the preloaded page has **no `nextCid`**, it contains all posts — no pagination needed - If `nextCid` **is present**, additional pages must be fetched via `community.posts.getPage({ cid: nextCid })` - `community.posts.pageCids.active` provides the CID of the first active-sorted page, which is the sort order the board manager needs Reference: `pkc-js/src/community/community-client-manager.ts`, `pkc-js/src/runtime/node/community/local-community.ts` ### Module flow ``` 1. Create PKC instance internally from the provided pkcRpcUrl 2. Load state JSON; get or create signer for this board via `pkc.createSigner()` 3. Get board (`LocalCommunity` or `RpcLocalCommunity`) 4. Check board roles via `community.roles`; if missing, call `community.edit()` to add as mod 5. Acquire file lock to prevent concurrent board managers on same board 6. Call `community.update()` 7. On each 'update' event: a. Determine thread source (three scenarios): 1. pageCids.active exists → fetch via getPage(), paginate via nextCid 2. Only pages.hot exists → use preloaded page, sort by lastReplyTimestamp desc then postNumber 3. Neither exists → no posts, return early b. Walk through pages to build full ordered list of threads c. Filter out pinned threads d. For each non-pinned thread beyond position (per_page * pages): - Skip if already archived - createCommentModeration({ archived: true }) and publish - Record archivedTimestamp in state file e. For each thread with replyCount >= bump_limit: - Skip if already archived - createCommentModeration({ archived: true }) and publish - Record archivedTimestamp in state file f. For each archived thread where (now - archivedAt) > archive_purge_seconds: - createCommentModeration({ purged: true }) and publish - Remove from state file g. For each author-deleted comment/reply: - createCommentModeration({ purged: true }) and publish 8. Every PURGE_SWEEP_INTERVAL_SECONDS, independent of update events (skipped when set to 0): - Run step (f) again, so time-based purging still happens on a quiet board ``` ### Key pkc-js APIs used | API | Purpose | |-----|---------| | `pkc.createCommentModeration()` | Archive and purge threads | | `commentModeration.publish()` | Publish the moderation action | | `community.posts.pageCids.active` | Get active sort page CID | | `community.posts.pages.hot` | Preloaded first page (for calculating active sort) | | `community.on('update', ...)` | Listen for new posts/updates | | `page.nextCid` | Paginate through multi-page feeds | ### Key pkc-js source files (reference only, not modified) | File | Relevant code | |------|--------------| | `src/pkc/pkc.ts` | `createCommentModeration()` definition | | `src/publications/comment-moderation/schema.ts` | ModeratorOptionsSchema with `archived`, `purged` fields | | `src/runtime/node/community/local-community.ts` | Existing archived check that blocks replies | | `src/runtime/node/community/db-handler.ts` | `queryPostsWithActiveScore()` — active sort CTE | | `src/pages/util.ts` | Sort type definitions and scoring functions |