<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Max</title>
    <description>The latest articles on DEV Community by Max (@maxonb).</description>
    <link>https://dev.to/maxonb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4019198%2Ff1c9909f-cef8-446c-8bad-c0ef347755e2.png</url>
      <title>DEV Community: Max</title>
      <link>https://dev.to/maxonb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maxonb"/>
    <language>en</language>
    <item>
      <title>I built a 5-agent swarm that researches crypto tokens and refuses to hallucinate numbers</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Tue, 07 Jul 2026 09:44:24 +0000</pubDate>
      <link>https://dev.to/maxonb/i-built-a-5-agent-swarm-that-researches-crypto-tokens-and-refuses-to-hallucinate-numbers-39b1</link>
      <guid>https://dev.to/maxonb/i-built-a-5-agent-swarm-that-researches-crypto-tokens-and-refuses-to-hallucinate-numbers-39b1</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Legate&lt;/strong&gt; — 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  One prompt is the wrong shape
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Legate splits the job across five roles, run in sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Commander → Scout → Analyst → Sentinel → Synthesizer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Commander&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scout&lt;/strong&gt; takes news and narrative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyst&lt;/strong&gt; is pure quantitative: price, liquidity, TVL, supply, holder concentration. No web search allowed. Structured data only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentinel&lt;/strong&gt; is social pulse: Twitter, Reddit, governance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synthesizer&lt;/strong&gt; merges the three findings into one briefing whose sections adapt to the topic type.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agents never call an API
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// every source returns WithFreshness&amp;lt;T&amp;gt; | null — it never throws&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WithFreshness&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Price&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules make this hold up:&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Three bugs that taught me the most
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Derive the chain from the address, never trust the model.&lt;/strong&gt;&lt;br&gt;
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 &lt;em&gt;only&lt;/em&gt; be Solana, and a &lt;code&gt;0x…&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A reasoning model treats recent dates as "the future."&lt;/strong&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Untrusted data needs a fence.&lt;/strong&gt;&lt;br&gt;
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 &lt;code&gt;&amp;lt;UNTRUSTED_DATA&amp;gt;&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it produces
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It's live and free during the beta if you want to poke at it: &lt;strong&gt;&lt;a href="https://www.legatelabs.xyz" rel="noopener noreferrer"&gt;legatelabs.xyz&lt;/a&gt;&lt;/strong&gt;. You can also point your own Telegram bot at the same engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

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

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>crypto</category>
    </item>
  </channel>
</rss>
