How do you form a group nobody can admit they're in?
Arun invoiced a design agency ₹1,20,000 in January. It's August. He is in a 4,000-member designers' Discord. He could post the agency's name right now and warn everyone. He won't, and you already know why: the freelancer who publicly names a client stops getting briefs. He'd pay for it alone, and everyone else would benefit. Here's the part that makes it a systems problem rather than a sad story. Three other people in that same Discord are owed money by that same agency. None of them knows. Each one is running the same arithmetic Arun is, arriving at the same answer, and saying nothing. Four people who together have real leverage. Individually, none of them can afford the first move. I built an agent for this over a hackathon weekend. The interesting part wasn't the AI. It was that every obvious solution destroys the thing you're trying to protect. The obvious version, and why it dies "Just make a private channel for victims of bad clients." To join, you say who burned you. Now the group knows. One screenshot and Arun is on a list. "Okay, collect reports centrally and only reveal at a threshold." Better. This is roughly how Callisto Vault handles assault reports, and it's a good pattern. But it reveals the group to its own members at the threshold. Four people now know each other's names and amounts. Four times the leak surface, arriving exactly when things get tense. The requirement I ended up with was stricter than I expected: Nobody is exposed. Not to the channel, not to the accused, and not to each other — not even after it works. Which sounds impossible, because how do four people coordinate if they can't know who they are? They don't. The agent knows. Nobody else does. The public board that can't name the client Here's what actually appears in the Discord: PICKET · matter #1 > "invoiced in January, still chasing in August" ₹50k–2L · 180d+ overdue 🟩⬜⬜⬜ 1/4 joined [ JOIN ] One sentence Arun wrote himself. An amount band, not his figure. A counter. The agency's name is nowhere on it, in any state, ever. That's the whole trick, and it's easy to misread as "we're being cautious". It isn't caution. The accused agency might be in that Discord. The board has to be safe to read for the very person it's about. So the board can't tell you who it is. Which raises the obvious problem: how does a real fellow-victim recognise it? They don't recognise it. They guess. And then they prove it. You tap JOIN, and the agent DMs you the intake steps: email us, and name the client yourself. That's the admission test. Not a password, not an invite code. It's the one thing only someone in the same situation would know. Get it right and your pip fills. Get it wrong and your claim is held, sealed, and nobody is told anything. Crucially you don't learn whether you guessed right, so nobody can use the system to fish for the name. The column I refused to write When you tap JOIN, the bot knows your Discord identity. When you email, it knows your address. Storing discord_id → claimant_id would be one line and would make a dozen features easier. That line is the entire attack surface. One leaked table and every claimant is public. So the bot never says who tapped, and the join reply is written to be impossible to personalise: def render_join_instruction(v: MatterPublic, intake_address: str) -> str: # Deliberately does NOT name who tapped: a Discord button tap is invisible # to other members, and naming the tapper here would be the exact # first-mover exposure this system exists to prevent. There's a side effect I like: you can tap JOIN twice and the bot won't remember you. It looks like a bug. It's the guarantee, visible. To recognise you, it would have to have stored the link it promised not to store. Negotiating for people you never introduce At four matching claims, one email goes to the agency: four documented claims, the combined total, the age of the oldest. No names. The agency offers 60%. And this is where the design either holds or collapses. The tempting move is a group thread: "they've offered 60%, what do we all think?" Convenient, and it hands everyone's identity to everyone else at the worst possible moment. Instead each claimant gets their own sealed email: For your claim it means: ₹72,000 (60% of your ₹1,20,000) Their slice. Their decision. Three accept, one refuses. Three claims settle, one stays open independently. Nobody was outvoted, because there was never a vote. The client is told "3 of 4 accepted", not which three. They still don't know each other. That's not a limitation of the build. That's the product. Two problems I didn't see coming Who decides admission? Matching "that media agency in Andheri, the blueprint one" to Blueprint Media Pvt. Ltd. is exactly what an LLM is good at. So my first version asked the model: does this claim belong? That makes a hallucination-prone component the doorman for a group of people trying to stay hidden. The fix is one word. The model is never asked "should this be let in?", only "which of these existing things does it look like, if any?" Admission is then if key != matter.canonical_key. I tested it with "Blueprint Constructions". Same first word, different company. Returns None. Not because the prompt asks nicely, but because the comparison happens in Python. Same rule everywhere: the model can read a messy claim, read the attached invoice, classify a reply. It cannot let anyone in, move any state, or put a number in an outgoing email. When the client writes "sixty percent", the 60 is only used because code checks that number literally appears in their own sentence. Why should the agency believe any of this? Their reasonable reply to "four people are owed money" is: prove it isn't one person with four inboxes and a good afternoon. I reached for a hash chain first. It proves nothing. I control the whole thing and can rebuild it in a second with any timestamps I want. It's a diary that catches you erasing a page, written by the person holding the pen. What saved it was already there: the comms gateway stamps every event with a monotonic sequence number that I don't issue and can't move. So every ledger row stores the seq that caused it, and a verifier re-fetches them: [3] independence: last claim admitted at seq 21392, first client REPLY at seq 21420 -> claims PRECEDE the client's first reply ✓ The four claims existed before the agency was ever contacted, and that ordering is enforced by infrastructure I don't own. They don't have to trust me. They have to trust that I can't rewrite someone else's log. If your system makes a claim about time or order, anchor it to a counter you don't control. Three things that bit me A card body that vanished. Posting a Discord card with a body rendered the title and button and silently dropped the body. Because edits are text-only, the board looked correct from its second update onward — so the bug only appeared on a tile's very first render. Which is the exact frame a demo video opens on. A poller that ate its own history. My event cursor defaulted to 0. Delete the local DB, restart, and the agent replays every event the project has ever seen, reopening closed cases and re-posting their public messages. If your poller has a cursor, decide what a missing cursor means, and make it mean "now". Being right and not recognising it. I added a vision model that reads the attached invoice and rejects a claim if the document disagrees. Then a test claim said ₹95,000 while its invoice said ₹1,20,000, and my system refused it. I debugged the "bug" for a minute before realising it was working. The thing worth taking The hard constraint turned out to be generative rather than limiting. Nobody may be exposed to anybody forced the amount bands, the name-it-yourself admission test, the column I didn't write, and the sealed per-person negotiation. Every one of those is a better design than what I'd have built without the constraint. If you're building something where people have to cooperate but can't afford to be seen cooperating — whistleblowing, wage disputes, deposit disputes, bad-landlord registries, the shape is reusable: a public counter, a private admission test only a real participant can pass, and an agent that acts on the group's behalf without ever assembling it. Code: github.com/BigAchiever/Picket What would you have done differently? I'm most unsure about the guess-and-prove admission test — it works, but it assumes enough density that guessing is worth someone's time.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to