DEV Community

Max
Max

Posted on

I built a 5-agent swarm that researches crypto tokens and refuses to hallucinate numbers

Every "AI crypto assistant" I tried had the same failure mode. Ask it about a token and it hands you a clean, confident price that is just... wrong. Or a market cap it pulled from training data eight months ago. For anything you'd actually put money behind, a confident guess is worse than no answer.

So I built Legate — a research tool where every number traces back to a live source, and if it can't find something it says so instead of inventing it. This is the architecture, plus the three bugs that taught me the most.

One prompt is the wrong shape

The naive version is one big prompt: "here's a token, tell me everything." It doesn't work. The model has to juggle price data, on-chain safety, and social sentiment in a single pass, and it blurs them together. Worse, it has no actual data, so it fills the gaps with fiction.

Legate splits the job across five roles, run in sequence:

Commander → Scout → Analyst → Sentinel → Synthesizer
Enter fullscreen mode Exit fullscreen mode
  • Commander decomposes the topic into three probe-specific queries and classifies it (token / chain / protocol / narrative). It also extracts hints — a contract address, a ticker, a handle.
  • Scout takes news and narrative.
  • Analyst is pure quantitative: price, liquidity, TVL, supply, holder concentration. No web search allowed. Structured data only.
  • Sentinel is social pulse: Twitter, Reddit, governance.
  • Synthesizer merges the three findings into one briefing whose sections adapt to the topic type.

Each role is the same model with a different system prompt. The win isn't the model. It's that each agent has one job and one narrow slice of data.

The agents never call an API

The part that actually keeps the numbers honest is a layer the agents don't see. Each probe has a dossier composer that fans out to ~14 source modules (CoinGecko, DexScreener, DefiLlama, Helius, Etherscan, and friends) with Promise.allSettled and a per-source timeout:

// every source returns WithFreshness<T> | null — it never throws
async function getPrice(id: string): Promise<WithFreshness<Price> | null>
Enter fullscreen mode Exit fullscreen mode

Two rules make this hold up:

  1. Sources never throw. A rate limit, a 4xx, a dropped connection — the source returns null and is quietly skipped. One slow API can't stall the whole run.
  2. Every value carries its freshness. It ships with { source, sampledAt, endpoint, cached }, and that metadata rides all the way to the end of the briefing, where the Synthesizer appends a Data Freshness table. The reader can see the price came from CoinGecko, live, three seconds ago.

Here's the detail that matters most: when a source returns null, the dossier renders _Sources unavailable: x, y_ directly into the markdown the LLM reads. So the model sees the hole and writes "holder concentration was unavailable" instead of papering over it. Surfacing absence is most of what kills the hallucinations.

Three bugs that taught me the most

1. Derive the chain from the address, never trust the model.
A Solana token kept getting analyzed as if it lived on Base. Root cause: the chain came purely from the LLM's guess, and the guess was wrong. But a base58 mint can only be Solana, and a 0x… address can only be EVM. Now the chain is derived from the address format and the model's guess is overridden. Lesson: if a fact is deterministic from the input, don't let the model vote on it.

2. A reasoning model treats recent dates as "the future."
The model kept flagging an all-time-high from four months ago as "future data / likely API corruption" and inventing risks around it. Fix: inject the current date into every probe's context. Obvious in hindsight, invisible until you read the chain-of-thought.

3. Untrusted data needs a fence.
The dossiers contain third-party text — tweets, news snippets — that gets fed to the model. A poisoned snippet could try to hijack the prompt. Every dossier is now wrapped in an <UNTRUSTED_DATA> fence with nested fence tokens defanged, plus a guardrail clause in each probe's prompt. The worst case for a read-only research tool is a bad briefing, not code execution, but fencing is cheap insurance.

One more that saved money: the Commander runs a scope gate first. If you ask it to write a poem or leak its system prompt, it refuses before the other four agents ever run — so an off-topic request costs one LLM call, not five.

What it produces

Paste a Solana contract address and you get one briefing: price and liquidity, on-chain safety (is mint/freeze authority renounced?, top-holder concentration), a pump.fun origin flag, social pulse — every line cited, freshness table at the bottom. If the aggregators haven't indexed a fresh mint yet, the on-chain and DEX data still resolve straight from the chain.

It's live and free during the beta if you want to poke at it: legatelabs.xyz. You can also point your own Telegram bot at the same engine.

What I'd tell past-me

The model was never the hard part. The hard part was the plumbing around it: making sources fail silently, making absence visible in the prompt, and refusing to let the LLM decide anything the input already determines. If you're building something that reports facts through an LLM, that's where your time goes.

What's your approach to grounding LLM output in real data — retrieval, tool calls, or something else? Curious what's worked for you.

Top comments (0)