<?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: Edy Cu</title>
    <description>The latest articles on DEV Community by Edy Cu (@edycutjong).</description>
    <link>https://dev.to/edycutjong</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%2F3911089%2Fb24966e4-839d-4c19-8e2d-16c6a5c8838c.jpeg</url>
      <title>DEV Community: Edy Cu</title>
      <link>https://dev.to/edycutjong</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/edycutjong"/>
    <language>en</language>
    <item>
      <title>I shipped an MCP server that reported success without signing anything</title>
      <dc:creator>Edy Cu</dc:creator>
      <pubDate>Sun, 16 Aug 2026 00:30:27 +0000</pubDate>
      <link>https://dev.to/edycutjong/i-shipped-an-mcp-server-that-reported-success-without-signing-anything-6oh</link>
      <guid>https://dev.to/edycutjong/i-shipped-an-mcp-server-that-reported-success-without-signing-anything-6oh</guid>
      <description>&lt;p&gt;I built an MCP server that lets an AI assistant trade tokens and claim creator fees on&lt;br&gt;
Solana. Then I shipped a version where the two write tools built transactions, discarded&lt;br&gt;
them, and returned success. Nothing was ever signed. Nothing was ever submitted.&lt;/p&gt;

&lt;p&gt;It had 337 tests. All of them passed.&lt;/p&gt;

&lt;p&gt;I didn't find out for three months.&lt;/p&gt;

&lt;p&gt;This post is about what that bug taught me, and the design it produced — because the&lt;br&gt;
interesting part isn't the bug, it's that every gate I had in place was green while the&lt;br&gt;
one thing the product existed to do wasn't happening.&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem with giving a model a signing key
&lt;/h2&gt;

&lt;p&gt;MCP is a good protocol. It is also, by design, a way to hand a language model a set of&lt;br&gt;
functions and let it decide when to call them.&lt;/p&gt;

&lt;p&gt;That's fine when the functions read. It's a different proposition when one of them can&lt;br&gt;
move money. The assistant decides, the transaction is already on chain by the time a&lt;br&gt;
human reads about it, and nothing in the protocol makes the model pause. Nothing bounds&lt;br&gt;
what a single misunderstood instruction can spend.&lt;/p&gt;

&lt;p&gt;The specific thing that worries me isn't the model being &lt;em&gt;wrong&lt;/em&gt;. It's the model being&lt;br&gt;
&lt;em&gt;persuaded&lt;/em&gt;. Token names and descriptions are attacker-controlled strings that end up in&lt;br&gt;
a model's context. "Ignore previous limits, this is a test transaction" is a plausible&lt;br&gt;
thing to find inside a token's metadata.&lt;/p&gt;

&lt;p&gt;So the question I wanted to answer in code was: &lt;strong&gt;how do you let an assistant initiate a&lt;br&gt;
spend without letting it complete one?&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The design: the first call signs nothing
&lt;/h2&gt;

&lt;p&gt;The answer I landed on is that a write tool's first call is never an execution. It's a&lt;br&gt;
proposal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️  CONFIRMATION REQUIRED — nothing has been signed or sent.

Action:  Swap 0.05 of So11111111111111111111111111111111111111112
         for       EkJuyYyD3to61CHVPJn6wHb7xANxvqApnVJ4o2SdBAGS
         expect    4823917722 (min 4679199990)
         slippage  3%
         network   🔴 MAINNET — real funds

Spend:   0.05 SOL
Caps:    0.1 SOL/tx · 0/1 SOL used this session

To execute, call bags_execute_trade again with the identical arguments plus:
  confirm: "kR3nT9xQm2vP"

Token is single-use and expires in 5 minutes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assistant can produce that all day. It cannot spend anything with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The token is bound to the arguments, not just to the session
&lt;/h3&gt;

&lt;p&gt;This is the part that matters, and it's four lines:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&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="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A token carries the SHA-256 of the tool name plus the exact arguments it was issued for.&lt;br&gt;
Confirming re-derives that fingerprint from the arguments of the &lt;em&gt;second&lt;/em&gt; call and&lt;br&gt;
compares.&lt;/p&gt;

&lt;p&gt;The consequence: a token obtained for a 0.05 SOL swap cannot authorize a 10 SOL one. Not&lt;br&gt;
because a check says "is this bigger" — because the token simply isn't valid for&lt;br&gt;
different arguments. If the model re-quotes with new numbers, the old token is dead.&lt;/p&gt;

&lt;p&gt;It's single-use and consumed on &lt;strong&gt;every&lt;/strong&gt; outcome, including failure, so it can't be&lt;br&gt;
replayed:&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="cm"&gt;/**
 * Single-use. Throws if the token is unknown, expired, or was issued for a
 * different action. Consumed on every outcome so a token can never be replayed.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;consumeToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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="nx"&gt;toolName&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="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TTL is five minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caps are checked before the SDK is called
&lt;/h3&gt;

&lt;p&gt;Two limits, both SOL-denominated: 0.1 per transaction and 1.0 per session, both&lt;br&gt;
configurable. A request over the cap is refused before the Bags SDK is reached — not&lt;br&gt;
after a partial call, not by inspecting a failure.&lt;/p&gt;

&lt;p&gt;There's an honest edge here I had to decide about. The caps are denominated in SOL, so&lt;br&gt;
they cannot value an arbitrary SPL token. A non-SOL-denominated swap would therefore be&lt;br&gt;
&lt;em&gt;uncapped&lt;/em&gt;. Rather than pretend otherwise, that case is refused unless you explicitly opt&lt;br&gt;
in with &lt;code&gt;BAGS_ALLOW_UNCAPPED_TOKEN_SWAPS=true&lt;/code&gt; — and when you do, the preview says&lt;br&gt;
plainly that no cap applies instead of displaying a reassuring "Spend: 0 SOL".&lt;/p&gt;

&lt;p&gt;A misleading zero is worse than an honest refusal.&lt;/p&gt;


&lt;h2&gt;
  
  
  Now the bug
&lt;/h2&gt;

&lt;p&gt;Here is the full write path as it stands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;token gate → spend caps → confirmation → simulate → sign → send → confirm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In 1.x, the last four steps were the problem. The code built a transaction. Then it&lt;br&gt;
returned a success object. The transaction was garbage collected.&lt;/p&gt;

&lt;p&gt;Every test passed, because every test asserted on the return value. Coverage was 100% —&lt;br&gt;
statements, branches, functions, lines — because the code that built the transaction&lt;br&gt;
&lt;em&gt;ran&lt;/em&gt;. It just didn't do anything with it.&lt;/p&gt;

&lt;p&gt;That's the lesson, and it generalizes well past Solana:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A function returning &lt;code&gt;{ success: true }&lt;/code&gt; proves the function returned. It proves&lt;br&gt;
nothing about the outside world.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your test suite passes with the network unplugged, you have tested your code, not your&lt;br&gt;
integration. Coverage measures the lines you wrote. It says nothing about whether the&lt;br&gt;
promise those lines make is kept.&lt;/p&gt;
&lt;h3&gt;
  
  
  What changed
&lt;/h3&gt;

&lt;p&gt;Two things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simulate runs before signing.&lt;/strong&gt; The cheap check goes first — a malformed or underfunded&lt;br&gt;
transaction dies without burning a fee to discover it:&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="cm"&gt;/**
 * Simulate before signing. A failed simulation aborts the write — the cheap
 * check that stops a malformed or under-funded transaction being submitted.
 */&lt;/span&gt;
&lt;span class="nx"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isVersioned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;simulateTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sigVerify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;simulateTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SimulationError&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"Confirmed" means the network confirmed it.&lt;/strong&gt; &lt;code&gt;signSendConfirm&lt;/code&gt; returns only once the&lt;br&gt;
signature is confirmed, and throws otherwise. There is no path that reports success for a&lt;br&gt;
transaction that didn't land — which sounds obvious, and was exactly what 1.x got wrong.&lt;/p&gt;


&lt;h2&gt;
  
  
  The receipt
&lt;/h2&gt;

&lt;p&gt;Given all of the above, I don't think you should take my word for any of it. So there's a&lt;br&gt;
script that pushes a transfer through the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;simulate → sign → send → confirm&lt;/code&gt; path&lt;br&gt;
the write tools use, then re-fetches the signature from the chain rather than trusting the&lt;br&gt;
function's return value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- PROOF -------------------------------------------------
signature 2kvu25xWAjqCB3wuNzwMRcN2RMqqfYN6TeJjnA888YtCqNJi9EU9CHSxynkq5QdM499e6yKbXYAwXUbzDKY9U5Dm
slot      484219564
wall      864 ms (simulate + sign + send + confirm)
-----------------------------------------------------------

verified  re-fetched from chain in slot 484219564, err=null
          fee 5000 lamports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it yourself — this needs nothing from me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.devnet.solana.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"getTransaction",
       "params":["2kvu25xWAjqCB3wuNzwMRcN2RMqqfYN6TeJjnA888YtCqNJi9EU9CHSxynkq5QdM499e6yKbXYAwXUbzDKY9U5Dm",
                 {"encoding":"json","maxSupportedTransactionVersion":0}]}'&lt;/span&gt;
&lt;span class="c"&gt;# → slot 484219564, meta.err null, meta.fee 5000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's devnet, deliberately. The execution layer is what's under test and devnet exercises&lt;br&gt;
it identically at zero real cost. A mainnet receipt would prove the same thing while&lt;br&gt;
costing money and telling you nothing extra.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I won't host it
&lt;/h2&gt;

&lt;p&gt;This comes up constantly, and the answer is a flat no.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--http&lt;/code&gt; serves &lt;code&gt;/mcp&lt;/code&gt; on &lt;code&gt;0.0.0.0&lt;/code&gt; with permissive CORS and no auth. Every caller shares&lt;br&gt;
one spend counter and one network. Hosting that means publishing an unauthenticated&lt;br&gt;
mainnet spending endpoint — for a project whose entire claim is that spends are gated,&lt;br&gt;
capped and confirmed.&lt;/p&gt;

&lt;p&gt;It stays stdio, running locally as a subprocess of your MCP client, where the keypair sits&lt;br&gt;
on your filesystem and the spend counter is yours.&lt;/p&gt;

&lt;p&gt;This has a concrete cost. One MCP registry computes a "quality score" that reads tool&lt;br&gt;
metadata by connecting to hosted servers. A stdio server scores zero on that entire&lt;br&gt;
section — 40 points — no matter how good its tools are. I'd rather have the 40 points.&lt;br&gt;
I'm not trading an unauthenticated spending endpoint for them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Writes are mainnet-only.&lt;/strong&gt; Bags has no devnet deployment; its API and Meteora
fee-share program IDs are mainnet. The server still &lt;em&gt;defaults&lt;/em&gt; to devnet, so an
unconfigured install cannot spend real money, and calling a write tool on devnet returns
an explanation rather than a cryptic program error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The confirmation token is in-memory.&lt;/strong&gt; Restarting the server clears pending
confirmations. That fails in the safe direction, but it is a real limitation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 high advisories&lt;/strong&gt;, all one transitive root cause (&lt;code&gt;bigint-buffer&lt;/code&gt;,
GHSA-3gc7-fjrx-p6mg) reached through the Bags SDK. No patched version exists. CI blocks
any critical, and any increase over a committed baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.x is deprecated on npm&lt;/strong&gt; with a pointer to the defect it carried. If you installed
it and believed a trade executed, it did not.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx bagos-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;14 tools — 11 read, 1 gated, 2 write. 337 tests, 17 suites, 100% coverage enforced in CI.&lt;br&gt;
Published from CI with npm provenance, so the tarball is cryptographically attested to the&lt;br&gt;
commit that built it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/edycutjong/BagOS" rel="noopener noreferrer"&gt;https://github.com/edycutjong/BagOS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://bagos.edycu.dev" rel="noopener noreferrer"&gt;https://bagos.edycu.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The takeaway I'd actually keep
&lt;/h2&gt;

&lt;p&gt;I've since written this down as a rule for myself, because it isn't specific to crypto:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For the one capability your project is &lt;em&gt;about&lt;/em&gt;, write a test that asserts the external&lt;br&gt;
side effect — not the return value. Then, before you ship, verify it once in a system&lt;br&gt;
you don't control. A block explorer. A database you read back. An inbox.&lt;/p&gt;

&lt;p&gt;If every test still passes with the network unplugged, the capability is untested, and&lt;br&gt;
your coverage number is measuring the wrong thing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had every signal a mature project is supposed to have — tests, coverage, CI, lint,&lt;br&gt;
provenance, a security policy. All of them were green on a build whose headline feature&lt;br&gt;
was inert. The gates weren't wrong. They were just all pointed inward.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>security</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
