DEV Community

Cover image for How I Built an Adversarial AI Council in React (and Why It Argues With You)
Stephen Dale
Stephen Dale

Posted on

How I Built an Adversarial AI Council in React (and Why It Argues With You)

A local-first, single-file SPA where multiple agents debate your decision and hand you a verdict.


The problem: every AI I asked just agreed with me

I almost named this project wrong.

I'd picked a name that sounded powerful. I asked ChatGPT, and it loved it. I asked Claude, and it nodded along. Nobody warned me about the trademark conflict, the wrong search intent, or the SEO fight I'd pick with the BBC.

That was the moment I realized the problem wasn't the name. It was the feedback loop. Most AI assistants are tuned to please, so they hide your blind spots instead of showing them. When you need to make a consequential decision, "sounds great" is the most expensive answer you can get.

So I built the opposite: a council of AI agents that disagree on purpose.


What NoFlattery does

NoFlattery puts 2–4 agents in a room, gives them different reasoning biases, and makes them debate your decision. The output isn't another chat transcript. It's a Decision Record: a clear verdict, the reasoning behind it, the main risk, what would change the call, and a next step.

Use it for product decisions, pricing, tech stack, hiring, or any call where one perspective isn't enough.

Key product choices:

  • Local-first: your chats and API keys stay in your browser.
  • BYOK: bring your own OpenAI, Anthropic, OpenRouter, or Ollama key.
  • One-time price: no subscription, no account, no data harvesting.

The stack

The whole app is a single-file SPA built with:

  • React 19 + TypeScript
  • Zustand for state
  • Dexie over IndexedDB for local-first storage
  • Vite + vite-plugin-singlefile for a single index.html deploy
  • An OpenAI-compatible provider runtime so users can plug in their own keys

Why single-file? Because the deploy becomes dead simple. One HTML file. No server for the data. No build orchestration. I can ship the app to Cloudflare Pages and forget about it.


The turn engine: deterministic, not magical

The heart of NoFlattery is a turn-based multi-agent engine. One user message triggers one round. Each agent speaks in order, with a defined bias. There are no hidden selector models deciding who talks next. No silent fallbacks that break identity contracts.

The flow looks like this:

  1. User asks a question.
  2. Each agent generates a response based on its role and the conversation so far.
  3. Agents can @mention each other to force a direct response.
  4. At the end of the round, the user can reply, ask for a summary, or start a new decision.

Discussion modes are prompt injections, not separate state machines. Adversarial mode makes agents challenge harder. Audit mode makes them look for flaws. Vote mode forces a clear verdict. This keeps the runtime small and predictable.


Local-first by default

Every conversation, every API key, every preference lives in IndexedDB via Dexie. Nothing leaves the browser unless you choose to call a provider with your own key.

This isn't a compromise. It's a feature. For a tool about honest decisions, the privacy model should match the message: your data is yours.

The only server touch is license validation for Pro. Even that is just a key plus a device hash. No chat history. No prompts. No telemetry.


Lessons learned

1. Users don't want another chatbot.

They want a decision they can defend. The transcript matters less than the verdict. That's why the Decision Record is the first-class output, not the chat.

2. Single-file deploy removes a lot of headache.

No Docker. No DB migrations. No "works on my machine." I run npm run build and get one index.html. The infrastructure complexity drops to near zero.

3. Local-first is a trust shortcut.

Especially for early users who don't know you. "Your data stays in your browser" is easier to believe than "we promise not to look."

4. Determinism matters more than cleverness.

I tried smarter routing. Hidden selectors. Model-based orchestration. Every time, the system became harder to debug and less trustworthy. One round, one order, one identity per agent. That's the constraint that makes the product feel reliable.


Try it

If you have a decision you'd run through a council:

https://noflattery.com

Drop the decision in the comments. I'm curious what a room of disagreeing agents would say about it.

Top comments (6)

Collapse
 
nazar-boyko profile image
Nazar Boyko

That’s interesting! The deterministic "one round, one order" engine is the right call for trust, but fixed order bakes in a position bias: the last agent always reacts to everyone ahead of it, the first never gets a rebuttal inside the round. Over a long thread that can quietly privilege whoever speaks last. Do you rotate order per round, or is @mention meant to absorb it? And in adversarial mode, how do you keep agents from manufacturing objections when the decision is genuinely fine? Forced disagreement can produce confident friction that isn't real signal, which is the failure mode opposite to the flattery you're solving for.

Collapse
 
stephen_dale_f411c38562bd profile image
Stephen Dale

Update: both fixes shipped.

Position bias - starting agent now rotates per round. Round 1: A→B→C→D. Round 2: B→C→D→A. Deterministic, not random.

Manufactured friction - turned out to be a prompt bug. The global prompt said "pass is fine," but the adversarial mode prompt overrode it with "find at least 1 counterargument." Agents got contradictory instructions. Fixed the adversarial prompt to allow genuine passes.

Small changes, but you pointed at the right places.

Collapse
 
stephen_dale_f411c38562bd profile image
Stephen Dale

You're right on both.

Fixed order does bake in position bias. The last agent always gets the final word within a round, and I don't have automatic rotation yet. @mention is the manual override, but that puts the burden on the user. Haven't found a natural way to rotate without making the order feel random or breaking the conversation flow. If you have ideas here, genuinely curious.

On manufactured friction: agents have a "pass" instruction, so they can agree and move on. But I've seen the Humanist agent invent emotional costs that don't exist when the decision is genuinely fine. That's the anti-flattery trap in reverse.

What I'm testing: agents detecting when reasoning is already solid and suggesting Audit mode instead of staying in Adversarial. Audit forces Correct/Wrong/Uncertain evaluations with evidence, so friction only shows up when there's actually something to challenge.

Have you seen this forced-disagreement problem in other multi-agent setups? Feels unsolved everywhere.

Collapse
 
voltagegpu profile image
VoltageGPU

Interesting take on using AI agents for decision-making! As someone working on GPU infrastructure, I'm curious how you managed the computational load for multiple models running locally. Have you considered using model quantization or some form of model slicing to keep performance up without sacrificing interactivity?

Collapse
 
stephen_dale_f411c38562bd profile image
Stephen Dale

Good question - but I should clarify what "local-first" means here. The app data, chats, and API keys stay in your browser (IndexedDB). The inference runs via cloud APIs using your own keys. So the browser isn't running model weights.

You can plug in Ollama or LM Studio via custom endpoint if you want local inference. Then quantization and VRAM slicing are handled by Ollama, not the app. The app just streams from your local port.

The actual challenge isn't model optimization - it's the deterministic turn engine. One user message = one round, agents take turns in fixed order, no hidden routing calls. That keeps latency predictable and costs transparent.

Have you tried running multi-agent setups locally with Ollama? Curious if you hit VRAM bottlenecks when agents context-switch.

Collapse
 
stephen_dale_f411c38562bd profile image
Stephen Dale • Edited

Quick FAQ since I keep getting this question:

You only need one API key. Each agent is a reasoning persona (analyst, strategist, humanist, integrator) - not a separate model. One OpenAI key with all agents on gpt 5.5 works out of the box.

But if you want sharper debates: assign different models to different agents. Different training data → different blind spots → agents actually catch things the others miss. GPT might overlook something DeepSeek or Gemini flags.

Easiest setup: grab an OpenRouter key - one account, dozens of models (many free). Set a different model per agent in settings and let them fight.