<?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: Pugar Huda Mantoro</title>
    <description>The latest articles on DEV Community by Pugar Huda Mantoro (@hudapugar).</description>
    <link>https://dev.to/hudapugar</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%2F3908866%2F3241f367-dd23-456e-91e8-fea42efb7f52.jpg</url>
      <title>DEV Community: Pugar Huda Mantoro</title>
      <link>https://dev.to/hudapugar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hudapugar"/>
    <language>en</language>
    <item>
      <title>Invoice Chaser: an invoice tool that learns how each client pays (Walrus Memory + Tatum on Sui)</title>
      <dc:creator>Pugar Huda Mantoro</dc:creator>
      <pubDate>Wed, 04 Nov 2026 00:54:30 +0000</pubDate>
      <link>https://dev.to/hudapugar/invoice-chaser-an-invoice-tool-that-learns-how-each-client-pays-walrus-memory-tatum-on-sui-ji7</link>
      <guid>https://dev.to/hudapugar/invoice-chaser-an-invoice-tool-that-learns-how-each-client-pays-walrus-memory-tatum-on-sui-ji7</guid>
      <description>&lt;p&gt;Every freelancer has a client like Acme. The invoice is due on the 5th, nothing happens, you send a friendly nudge on the 6th, nothing happens, and somewhere around the 10th the money shows up, usually right after you finally write something formal. Next month it happens again, because your invoicing tool has no idea it already happened four times.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Invoice Chaser&lt;/strong&gt; for the Total Recall hackathon (Tatum × Walrus Memory) to fix exactly that. It's an invoice tracker for freelancers who get paid in USDC on Sui, and it remembers how each client pays.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://invoice-chaser-sigma.vercel.app" rel="noopener noreferrer"&gt;https://invoice-chaser-sigma.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Video (2:08): &lt;a href="https://youtu.be/nCpQtlcQF-0" rel="noopener noreferrer"&gt;https://youtu.be/nCpQtlcQF-0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/PugarHuda/invoice-chaser" rel="noopener noreferrer"&gt;https://github.com/PugarHuda/invoice-chaser&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;You issue an invoice. Instead of "300 USDC" the client is asked to pay something like &lt;strong&gt;300.004217 USDC&lt;/strong&gt;. Those extra micro-decimals are the invoice's fingerprint on-chain.&lt;/li&gt;
&lt;li&gt;The client opens a pay link with a real &lt;code&gt;sui:pay&lt;/code&gt; QR code and pays from any Sui wallet. The payment goes through the &lt;strong&gt;Sui Payment Kit&lt;/strong&gt; registry with the invoice as the nonce, so the chain refuses a second payment for the same invoice, and the app finds the payment by invoice on its own. Every payment is checked &lt;strong&gt;on Sui Mainnet through Tatum's gRPC gateway&lt;/strong&gt;: did it succeed, how much USDC landed on my address, is it newer than the invoice, has this transaction been used before, and which wallet paid.&lt;/li&gt;
&lt;li&gt;Everything that happens (invoice sent, reminder sent and in what tone, paid how many days late, underpaid by how much, and any rule you tell it) is written to &lt;strong&gt;Walrus Memory&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Before the next reminder, the app recalls that client's history and decides &lt;strong&gt;when&lt;/strong&gt; to chase, &lt;strong&gt;in what tone&lt;/strong&gt;, and &lt;strong&gt;what it expects&lt;/strong&gt; to happen. A "Why?" panel shows the memories behind each decision.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The part judges asked for: does it actually learn?
&lt;/h2&gt;

&lt;p&gt;Here is the same invoice for Acme (due 5 Nov) with memory switched off and on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Memory off&lt;/th&gt;
&lt;th&gt;Memory on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First reminder&lt;/td&gt;
&lt;td&gt;6 Nov, friendly&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9 Nov, formal&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predicted payment&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 Nov&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Why&lt;/td&gt;
&lt;td&gt;default schedule&lt;/td&gt;
&lt;td&gt;Acme paid +4, +6, +5, +5 days late; formal reminders were followed by payment within 3 days 4/4 times, friendly 1/3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the prediction got better with each invoice it saw. Before each of Acme's last four payments the app predicted how late it would be. It was off by &lt;strong&gt;4 days, then 2, then 0, then 0&lt;/strong&gt;. That chart is in the app, on every client page.&lt;/p&gt;

&lt;p&gt;Rules work the same way. Type "never chase Acme before the 10th" into the client page and it becomes a memory; the next plan moves the reminder to the 10th, and the Why panel says so. Or paste the client's email: Walrus Memory's &lt;code&gt;analyze()&lt;/code&gt; pulls out "vendor payments are only run on Thursdays and never before the 15th" as a fact, and the plan predicts a Thursday payment and holds reminders until the 15th, even for a client with no payment history yet. (My first parser read that sentence as "don't remind on Thursdays". A Playwright QA pass caught it.)&lt;/p&gt;

&lt;h2&gt;
  
  
  How it uses Walrus Memory
&lt;/h2&gt;

&lt;p&gt;I didn't want memory to be a log on the side, so &lt;strong&gt;Walrus Memory is the only database&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every event is one memory: a sentence a human can read, plus a small JSON tag the code can parse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acme Corp paid inv-095 in full (3500.007537 USDC) on 2026-10-20, 5 day(s) after due, after 1 reminder(s); last tone formal.
#ic {"type":"invoice_paid","client":"c1","invoice":"inv-095","delay_days":5,"reminders":1,"last_tone":"formal",...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each client gets its own namespace (&lt;code&gt;ic:main:client:&amp;lt;id&amp;gt;&lt;/code&gt;). When the server starts it holds nothing. It lists the namespaces, recalls each one, and rebuilds clients, invoices and learned profiles from Walrus. There's a &lt;strong&gt;Total Recall&lt;/strong&gt; button that throws away local state and does it again on demand. This also happens to be the only way to run on the Tatum AI Builder preview, which has no database at all.&lt;/p&gt;

&lt;p&gt;Two practical things I learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Mainnet write takes ~25 seconds end to end (encrypt, embed, store, certify). So writes go into a background queue; the UI says &lt;em&gt;writing to Walrus…&lt;/em&gt; until the relayer confirms, then links the blob on Walruscan. Nothing is ever shown as saved before it is.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;recall&lt;/code&gt; is similarity-ranked and capped at 100, so it's not an inventory. I use &lt;code&gt;listNamespaces&lt;/code&gt; to know how many memories &lt;em&gt;should&lt;/em&gt; exist, and if a recall comes back short the app refuses to settle payments rather than risk accepting a digest it can't see was already used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo history (Aug–Oct, 38 memories) was written with a seed script that uses the app's own format. Every seeded memory carries &lt;code&gt;"seed": true&lt;/code&gt; and the UI labels it. Anything you do in the app is a real new memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it uses Tatum
&lt;/h2&gt;

&lt;p&gt;All chain reads go through Tatum's &lt;strong&gt;Sui Mainnet gRPC gateway&lt;/strong&gt; (&lt;code&gt;sui-mainnet-grpc.gateway.tatum.io&lt;/code&gt;, &lt;code&gt;x-api-key&lt;/code&gt; header), with &lt;code&gt;SuiGrpcClient&lt;/code&gt; from &lt;code&gt;@mysten/sui&lt;/code&gt; over native gRPC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LedgerService.GetTransaction&lt;/code&gt;: verify a payment from &lt;code&gt;effects.status&lt;/code&gt; and &lt;code&gt;balance_changes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;StateService.GetBalance&lt;/code&gt;: watch the payee's USDC balance&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LedgerService.GetCheckpoint&lt;/code&gt;: the live proof page&lt;/li&gt;
&lt;li&gt;building the payer's Payment Kit transaction (coin selection, gas) and executing an agent's signed payment&lt;/li&gt;
&lt;li&gt;looking an invoice's payment up in the Payment Kit registry, and the payer's SuiNS name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sui's JSON-RPC was deprecated during the hackathon, and gRPC has no "list transactions for an address" call. The Sui Payment Kit fixed that for me: a payment is a record keyed by the invoice, so I look it up directly instead of scanning history. (One gotcha: the SDK's &lt;code&gt;getPaymentRecord&lt;/code&gt; builds a malformed type name, &lt;code&gt;PaymentKey&amp;lt;phantom T&amp;gt;&amp;lt;coin&amp;gt;&lt;/code&gt;, so I query the dynamic field myself.) The free plan allows 3 requests per second, so calls are throttled and 429s are retried with backoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents can pay too (x402)
&lt;/h2&gt;

&lt;p&gt;Every invoice is also an HTTP resource. An accounts-payable agent that requests it gets &lt;code&gt;402 Payment Required&lt;/code&gt; with the terms (USDC on Sui, amount, payee, and the Payment Kit registry + nonce), pays, and retries with &lt;code&gt;X-PAYMENT: &amp;lt;digest&amp;gt;&lt;/code&gt; to get a receipt. The repo has a reference agent that checks its own budget before paying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tested on testnet first
&lt;/h2&gt;

&lt;p&gt;The same code runs against Sui Testnet with one env var (Tatum's testnet gateway, Circle testnet USDC, testnet Payment Kit) and a separate memory namespace. An end-to-end script pays a real invoice as an x402 agent, then checks on-chain that a second payment is rejected, that an unreported payment is found and settled automatically, and that a replayed transaction is refused. All six checks pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then on Mainnet
&lt;/h2&gt;

&lt;p&gt;On Sui Mainnet, an agent asked for invoice &lt;code&gt;inv-mvzyioev&lt;/code&gt;, got &lt;code&gt;402 Payment Required&lt;/code&gt;, noticed it held no USDC, swapped just enough SUI through the Cetus aggregator, paid through the Payment Kit registry and got its receipt (&lt;a href="https://suiscan.xyz/mainnet/tx/4BTevagvT34X166FBGsqj9tQSxisY1PDGwXXtrxLSC3z" rel="noopener noreferrer"&gt;payment&lt;/a&gt;). A second invoice was paid through the registry and nobody told the app; the production server found it by invoice nonce and settled it (&lt;a href="https://suiscan.xyz/mainnet/tx/G5r8eC1hwTkrj4Dg7FFWTcP1P5LTJaRn8FvGUx7WbAJP" rel="noopener noreferrer"&gt;payment&lt;/a&gt;). Delta Studio, a client with no history that morning, now has two remembered on-time payments, and its profile changed accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  And over MPP
&lt;/h2&gt;

&lt;p&gt;The same invoice is also a Machine Payments Protocol charge (&lt;code&gt;/api/chaser/mpp/&amp;lt;invoice&amp;gt;&lt;/code&gt;, built on &lt;code&gt;@suimpp/mpp&lt;/code&gt;). An agent got the &lt;code&gt;WWW-Authenticate: Payment&lt;/code&gt; challenge, paid 0.015645 USDC and received a &lt;code&gt;Payment-Receipt&lt;/code&gt; (&lt;a href="https://suiscan.xyz/mainnet/tx/CrgvCr2FoJXxuc3p3tuNorCSoPquSUeuuhHqu5tL8iG4" rel="noopener noreferrer"&gt;payment&lt;/a&gt;); the server verified it again through Tatum before remembering it.&lt;/p&gt;

&lt;h2&gt;
  
  
  No SUI needed, and no waiting
&lt;/h2&gt;

&lt;p&gt;Clients shouldn't need a second token just to pay gas. The pay page now builds the Payment Kit transaction with the server's wallet as gas owner: the payer signs, the server adds its signature and submits. And an open pay page listens to Tatum's gRPC checkpoint stream, so it flips to paid the moment the money lands. One Mainnet payment shows both: the payer's SUI balance didn't move, and the stream caught it at checkpoint 341405407 (&lt;a href="https://suiscan.xyz/mainnet/tx/8UkL1z4KhCrta1zJfvfTsdZmMPss9jrTnNcceLGUgwCq" rel="noopener noreferrer"&gt;payment&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents can read the memory too (MCP)
&lt;/h2&gt;

&lt;p&gt;The app is also an MCP server. &lt;code&gt;claude mcp add --transport http invoice-chaser https://invoice-chaser-sigma.vercel.app/api/chaser/mcp&lt;/code&gt; gives Claude 12 tools, like &lt;code&gt;client_profile&lt;/code&gt;, &lt;code&gt;plan_reminder&lt;/code&gt;, &lt;code&gt;chase_queue&lt;/code&gt; and &lt;code&gt;search_client_memory&lt;/code&gt;, over the same Walrus memories.&lt;/p&gt;

&lt;h2&gt;
  
  
  A statement you can send to the client
&lt;/h2&gt;

&lt;p&gt;Each client has a statement of account (JSON or CSV, and the &lt;code&gt;client_statement&lt;/code&gt; MCP tool): every invoice and payment from Walrus Memory, with each payment re-checked on Sui in one Tatum &lt;code&gt;BatchGetTransactions&lt;/code&gt; call and linked to its explorer page and the Walrus blob that remembers it. For the client with real Mainnet payments, all 7 come back verified on Sui.&lt;/p&gt;

&lt;h2&gt;
  
  
  Receipts that outlive the app
&lt;/h2&gt;

&lt;p&gt;When an invoice is paid, the app can render a PDF receipt and store it on Walrus as a blob for a year, then remember the blob id in the client's memory. The payer gets the same link on their pay page. &lt;a href="https://aggregator.walrus-mainnet.walrus.space/v1/blobs/maCqeFY1G5eSVx_ji5zrqXM308rCMUMY-nGWqBMNNPM" rel="noopener noreferrer"&gt;Here is one from Mainnet&lt;/a&gt;. The UI itself is also published as a Walrus Site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with Tatum AI Builder
&lt;/h2&gt;

&lt;p&gt;The first version came from one prompt in &lt;a href="https://ai.tatum.io" rel="noopener noreferrer"&gt;Tatum AI Builder&lt;/a&gt; (the prompt is in the repo, the generated commit is tagged &lt;code&gt;ai-builder-v0&lt;/code&gt;). It produced a clean UI with all five screens and a sensible profile algorithm. The backend was a mock: in-memory data, fake memories, a simulated wallet payment, and a QR code that was just an icon. I kept the UI and replaced every mock with the real thing: Tatum gRPC, Walrus Memory, Sui Payment Kit, and a payment check that can say no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits I know about
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reminders open in your email app with the pay link; the app does not send email itself.&lt;/li&gt;
&lt;li&gt;Rules are read with a small parser (day of month, pay days, days to avoid, tone). Stranger phrasing ("after their board meets on the first Tuesday") isn't understood yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to the Tatum and Walrus teams for the quick answers in Discord.&lt;/p&gt;

&lt;h1&gt;
  
  
  TotalRecall &lt;a class="mentioned-user" href="https://dev.to/tatum_io"&gt;@tatum_io&lt;/a&gt; @WalrusProtocol
&lt;/h1&gt;

</description>
      <category>walrus</category>
      <category>sui</category>
      <category>tatum</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>I let Stripe grade my tests: building a billing simulator where no expected value comes from my own code</title>
      <dc:creator>Pugar Huda Mantoro</dc:creator>
      <pubDate>Thu, 22 Oct 2026 05:12:30 +0000</pubDate>
      <link>https://dev.to/hudapugar/i-let-stripe-grade-my-tests-building-a-billing-simulator-where-no-expected-value-comes-from-my-own-4iil</link>
      <guid>https://dev.to/hudapugar/i-let-stripe-grade-my-tests-building-a-billing-simulator-where-no-expected-value-comes-from-my-own-4iil</guid>
      <description>&lt;p&gt;&lt;em&gt;Built for the TestSprite Hackathon, Season 4. Live: &lt;a href="https://tera-orpin-three.vercel.app" rel="noopener noreferrer"&gt;tera-orpin-three.vercel.app&lt;/a&gt; · Code: &lt;a href="https://github.com/PugarHuda/tera" rel="noopener noreferrer"&gt;github.com/PugarHuda/tera&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Founders are afraid to touch pricing, and they are right to be. Move a customer from $29 to $79 on day 17 of a 31-day month and Stripe writes two proration lines you have probably never computed by hand: an "Unused time" credit of −$14.03 and a "Remaining time" charge of $38.23, on top of the $79 renewal. Add a coupon that was applied once, a tax rate that is inclusive, a trial, or a subscription in Stripe's new flexible billing mode, and the invoice your customer sees is no longer something you can predict from the pricing page.&lt;/p&gt;

&lt;p&gt;Tera simulates it first. You give it a customer timeline (subscribe, change seats, upgrade, apply a coupon, pause, cancel) and it returns every invoice to the cent, plus the webhook events Stripe will send for it. This post is not really about the simulator, though. It is about the question every test suite eventually has to answer: &lt;strong&gt;who decides what the right answer is?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule: the engine never writes its own expectations
&lt;/h2&gt;

&lt;p&gt;The easiest way to get a green test suite for a billing engine is to run the engine, save its output, and assert that it does not change. That suite is worthless the day the engine is wrong, because it was wrong when the snapshot was taken too.&lt;/p&gt;

&lt;p&gt;So Tera has one rule. Every expected value is derived by hand from the billing rules, written next to its arithmetic, and &lt;strong&gt;then replayed in a real Stripe test-mode account with test clocks&lt;/strong&gt;. Scenario S05 in &lt;a href="https://github.com/PugarHuda/tera/blob/main/golden/scenarios.json" rel="noopener noreferrer"&gt;&lt;code&gt;golden/scenarios.json&lt;/code&gt;&lt;/a&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"math"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"remaining 15/31: -round(2900*15/31)=-1403, +round(7900*15/31)=3823"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"expect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2027-02-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;-1403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3823&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7900&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10320&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A script creates the same customer in Stripe, advances a test clock through the timeline, reads back Stripe's invoices and diffs them line by line against both the hand-derived values and Tera's engine. Only when all three agree is the scenario marked &lt;code&gt;verified&lt;/code&gt;, and the evidence (invoice ids, test clock id) is committed to the repo. Today there are 138 golden scenarios and all 135 timelines among them match Stripe to the cent. The other three are migration scenarios with no invoices to replay.&lt;/p&gt;

&lt;p&gt;When Stripe disagreed with my hand derivation, Stripe won, and I kept the receipts. A coupon applied in the same second as subscribing discounts the &lt;em&gt;next&lt;/em&gt; invoice, because Stripe finalizes the first one at creation. A &lt;code&gt;repeating&lt;/code&gt; coupon for 3 months lasts three calendar months, not three invoices. A percent discount on a proration comes off the rounded gross. None of those were in my head when I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting Stripe find the cases I did not think of
&lt;/h2&gt;

&lt;p&gt;Hand-written scenarios only cover what you already suspect. So Tera also runs &lt;strong&gt;differential fuzzing&lt;/strong&gt;: a seeded generator produces random customer timelines, with month-end anchors, Feb 29, random times of day, trials, seat and plan changes, coupons and both tax behaviours. Each one runs through Tera and through Stripe, and any difference is a finding.&lt;/p&gt;

&lt;p&gt;Every finding goes through the same pipeline: a targeted Stripe probe to isolate the rule, a new hand-derived golden scenario that pins it, then the engine fix. The fuzzer runs nightly in CI with fresh seeds, in both of Stripe's billing modes. The rules it surfaced are some of the most interesting in the repo. Stripe prorates the &lt;em&gt;unit&lt;/em&gt; price to 12 decimals before multiplying by seats, so a credit of exactly 11257.5 lands a cent under the tie. In flexible mode every credit refunds its original debit with the gross and the discount rounded separately.&lt;/p&gt;

&lt;p&gt;This week the same approach caught a real gap in usage-based billing. A code audit found the engine silently dropping metered usage on an immediate cancellation. Seven live probes gave the rules: tier lines run only up to the tier the usage reaches, a metered item has no unused-time line, and cancelling with &lt;code&gt;invoice_now&lt;/code&gt; bills the usage so far. A follow-up fuzz batch for metered prices then found that Stripe still sends a $0 final invoice after an unused last period. It also found a stale usage window in my own replay script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where TestSprite fits
&lt;/h2&gt;

&lt;p&gt;Stripe is the oracle for the engine. TestSprite is the gate for everything a user actually touches.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;28 UI tests on every pull request&lt;/strong&gt;, run by the TestSprite GitHub App against that PR's own Vercel preview. Branch protection requires the &lt;code&gt;TestSprite V3&lt;/code&gt; check. Each test asserts hand-derived amounts in the browser: "Invoice 2 totals $103.20", not "an invoice is shown".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tests live in the repo.&lt;/strong&gt; Every plan is a JSON file in &lt;code&gt;testsprite/plans/&lt;/code&gt; and every test has hand-written Playwright code in &lt;code&gt;testsprite/code/&lt;/code&gt;. A CI check compares both with the cloud project and fails on drift. That matters because an auto-healing run can quietly rewrite stored code, for example pinning a preview URL or loosening an assertion to two words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API tests against the preview, too.&lt;/strong&gt; TestSprite gives backend tests no target URL, so a small script stores each API test with its base URL pointing at the PR's preview, runs it with auto-heal off, and puts production back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triage that uses the oracle.&lt;/strong&gt; When &lt;code&gt;TestSprite V3&lt;/code&gt; fails, a workflow downloads the failure bundle, replays the golden scenarios behind that test on the same preview, and compares them with Stripe's recorded invoices. One PR comment then says either "engine still matches Stripe, so this is a UI regression" or which invoice the engine now gets wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A nightly money-math canary&lt;/strong&gt; on production, plus Playwright user journeys, Schemathesis fuzzing of the OpenAPI spec, Arazzo workflows, Lighthouse budgets and Stryker mutation testing on every PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TestSprite caught a real bug before merge on PR #18. A price added in the builder billed $0.00 instead of ¥4,500, because the builder put a metered price first. The fix went in, and the check went green.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong, honestly
&lt;/h2&gt;

&lt;p&gt;The workspace ran out of TestSprite credits twice during the hackathon. When that happens the check reports "Not enough credits" without running a single test. Nine pull requests hit that wall; the ones that shipped were merged by the repository admin after every other check had passed. They are not hidden: &lt;a href="https://github.com/PugarHuda/tera/blob/main/docs/LOOP.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/LOOP.md&lt;/code&gt;&lt;/a&gt; is generated from the GitHub API and lists every PR and every commit's TestSprite verdict. Five went red on real test results, all fixed in the same PR, and nine were blocked by credits. A credit shortfall is not a test verdict, and treating it as one in either direction would make the ledger lie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, Oct 23.&lt;/strong&gt; The credit boost arrived. &lt;code&gt;main&lt;/code&gt; now requires TestSprite V3, the TestSprite API tests (run against each PR's own preview), the unit tests and the browser journeys, and admins can no longer bypass them. To prove the gate holds, &lt;a href="https://github.com/PugarHuda/tera/pull/40" rel="noopener noreferrer"&gt;PR #40&lt;/a&gt; planted a one-line bug: proration truncated instead of rounded, so the S05 example billed $103.19 instead of $103.20. 105 unit tests failed, the browser journeys saw the wrong amounts on the preview, and 52 goldens disagreed with Stripe's own invoices. It was closed without merging. Every PR since has gone green through the full gate. One of them added Stripe's one-off invoice items, probed eight ways before any rule was written. The fuzzer then found one more rule: a coupon never discounts a one-off credit.&lt;/p&gt;

&lt;p&gt;A second lesson: every new test layer I added found something about the &lt;em&gt;previous&lt;/em&gt; layers. Schemathesis found the OpenAPI spec looser than the validator. Redocly Respect's strict schema checks found my conditional schemas rejecting properties their parent declared. Then my fix for that made Schemathesis unable to generate requests at all. Each layer is cheap. Not running the next one is what gets expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you build something similar
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pick an oracle that is not your code. For billing it is Stripe's test clocks. For tax it could be a published calculator. For physics, a reference implementation.&lt;/li&gt;
&lt;li&gt;Derive expectations by hand and write the arithmetic next to them. Future you will need it when the oracle disagrees.&lt;/li&gt;
&lt;li&gt;Let the oracle generate the questions too (differential fuzzing), and turn every disagreement into a pinned example.&lt;/li&gt;
&lt;li&gt;Keep your end-to-end tests in the repo and check the cloud copy against it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try it: open &lt;a href="https://tera-orpin-three.vercel.app/simulator?s=S05" rel="noopener noreferrer"&gt;the simulator on S05&lt;/a&gt;, change the upgrade date, and press &lt;strong&gt;Verify in Stripe&lt;/strong&gt;. Your own timeline gets replayed in a fresh test clock and diffed to the cent.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;AI disclosure: this post was drafted by an AI agent (Claude Code) from the project's own repository, evidence files and CI history, and published at the author's request. The project itself was built with the same agent.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>stripe</category>
      <category>typescript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Migrating Yearn Finance's Strategy Template from Brownie to ApeWorx Ape — A DeFi-Specific Case Study</title>
      <dc:creator>Pugar Huda Mantoro</dc:creator>
      <pubDate>Sat, 02 May 2026 10:48:14 +0000</pubDate>
      <link>https://dev.to/hudapugar/migrating-yearn-finances-strategy-template-from-brownie-to-apeworx-ape-a-defi-specific-case-study-4m57</link>
      <guid>https://dev.to/hudapugar/migrating-yearn-finances-strategy-template-from-brownie-to-apeworx-ape-a-defi-specific-case-study-4m57</guid>
      <description>&lt;h1&gt;
  
  
  Migrating Yearn Finance's Strategy Template from Brownie to ApeWorx Ape — A DeFi-Specific Case Study
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Codemod "Boring AI" Hackathon · DoraHacks 2026&lt;br&gt;
Source: &lt;a href="https://github.com/PugarHuda/brownie-to-ape" rel="noopener noreferrer"&gt;github.com/PugarHuda/brownie-to-ape&lt;/a&gt;&lt;br&gt;
Companion case study: &lt;a href="//./MEDIUM_ARTICLE.md"&gt;token-mix end-to-end (Medium)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why pick Yearn?
&lt;/h2&gt;

&lt;p&gt;Of the five OSS Brownie projects I validated this codemod against, &lt;a href="https://github.com/yearn/brownie-strategy-mix" rel="noopener noreferrer"&gt;&lt;code&gt;yearn/brownie-strategy-mix&lt;/code&gt;&lt;/a&gt; is the most production-relevant. Yearn Finance is one of the largest yield-aggregation protocols in DeFi, with billions in TVL at peak; this repo is the &lt;em&gt;official template&lt;/em&gt; every Yearn strategy developer forks to build a new strategy. If a codemod can clean-migrate this template, it can handle the strategy contracts that followed — and there are dozens of them across forks, partners, and audits.&lt;/p&gt;

&lt;p&gt;The template is also intentionally complex: it ships with a multi-network config, mainnet-fork test fixtures, whale-impersonation patterns for testing real-world deposits, and Brownie-specific testing idioms that exercise edge cases the codemod rarely sees in tutorial repos.&lt;/p&gt;

&lt;p&gt;This case study walks through the full migration: what the codemod handled, what required manual cleanup, and how the design of the codemod (FN-over-FP, AST-strict guards) interacted with Yearn-shaped code in particular.&lt;/p&gt;




&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;p&gt;Cloning the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/yearn/brownie-strategy-mix.git
&lt;span class="nb"&gt;cd &lt;/span&gt;brownie-strategy-mix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source structure (Python only — Solidity contracts are out of scope):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brownie-strategy-mix/
├── brownie-config.yaml
├── conftest.py
├── tests/
│   ├── test_operation.py
│   ├── test_revoke.py
│   ├── test_emergency_exit.py
│   └── conftest.py
└── scripts/
    └── deploy_strategy.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7 Python files, ~600 LOC total. Brownie idioms in use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;from brownie import accounts, network, chain, Contract, Wei&lt;/code&gt; — most of the standard import surface&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;accounts.at(address, force=True)&lt;/code&gt; — the whale-impersonation pattern (Yearn tests deposit large amounts from real holder addresses)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chain.sleep(N)&lt;/code&gt; and &lt;code&gt;chain.mine(N)&lt;/code&gt; — for time-locked exit fees&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;interface.IERC20(addr)&lt;/code&gt; — for ERC-20 balance assertions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tx.events["Transfer"].values()&lt;/code&gt; — for event verification&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pytest.fixture(autouse=True) def isolate(fn_isolation): pass&lt;/code&gt; — Brownie's chain-rewind boilerplate&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Running the codemod
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx codemod@latest @pugarhuda/brownie-to-ape &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;💥&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Workflow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;started&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;⏺&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Apply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Brownie&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Ape&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;transforms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"codemod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"brownie-to-ape"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"edits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"wei_rewritten"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"unknown_exceptions"&lt;/span&gt;&lt;span class="p"&gt;:[],&lt;/span&gt;&lt;span class="nl"&gt;"rewrote_brownie_attr"&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="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"codemod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"brownie-to-ape"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"edits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"wei_rewritten"&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="nl"&gt;"unknown_exceptions"&lt;/span&gt;&lt;span class="p"&gt;:[],&lt;/span&gt;&lt;span class="nl"&gt;"rewrote_brownie_attr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"codemod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"brownie-to-ape"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"edits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"wei_rewritten"&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="nl"&gt;"unknown_exceptions"&lt;/span&gt;&lt;span class="p"&gt;:[],&lt;/span&gt;&lt;span class="nl"&gt;"rewrote_brownie_attr"&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="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"codemod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"brownie-to-ape"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"edits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"wei_rewritten"&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="nl"&gt;"unknown_exceptions"&lt;/span&gt;&lt;span class="p"&gt;:[],&lt;/span&gt;&lt;span class="nl"&gt;"rewrote_brownie_attr"&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="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;✅&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Workflow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;completed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;successfully&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="err"&gt;s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;4 of 7 .py files modified, ~33 patterns auto-migrated&lt;/strong&gt;, 3 seconds.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git diff --stat&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; conftest.py                  | 11 ++++++-----
 scripts/deploy_strategy.py   |  4 ++--
 tests/conftest.py            | 13 ++++++-------
 tests/test_operation.py      | 24 +++++++++++++-----------
 4 files changed, 27 insertions(+), 25 deletions(-)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What the codemod handled cleanly
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Imports — multi-line, mixed types
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;brownie&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Wei&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ape&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ape.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;convert&lt;/span&gt;
&lt;span class="c1"&gt;# TODO(brownie-to-ape): no direct Ape equivalent for: Contract
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;network&lt;/code&gt; → &lt;code&gt;networks&lt;/code&gt; rename applied&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Wei&lt;/code&gt; removed from import line because the codemod auto-rewrites &lt;code&gt;Wei(...)&lt;/code&gt; to &lt;code&gt;convert(...)&lt;/code&gt; and adds the &lt;code&gt;from ape.utils import convert&lt;/code&gt; line&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Contract&lt;/code&gt; flagged as a TODO because it's an artifact name — the user must decide whether to use &lt;code&gt;ape.Contract&lt;/code&gt; (for arbitrary-address loading) or &lt;code&gt;project.&amp;lt;Name&amp;gt;&lt;/code&gt; (for compile-time artifacts). The codemod can't infer this without project schema.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The whale-impersonation idiom
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;whale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WHALE_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;force&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;whale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;impersonate_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WHALE_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;Pass 7b&lt;/strong&gt; of the codemod — the strict guard requires &lt;code&gt;force=True&lt;/code&gt; to be present (bare &lt;code&gt;accounts.at(addr)&lt;/code&gt; is a different operation in Ape, used for already-known accounts, and we don't want to over-rewrite). It fired correctly here because the Yearn test pattern always uses &lt;code&gt;force=True&lt;/code&gt; for whales.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Time and block control
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WAITING_PERIOD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pending_timestamp&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;WAITING_PERIOD&lt;/span&gt;
&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_blocks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;chain.sleep&lt;/code&gt; rewrite only fires on statement form (Pass 6); if &lt;code&gt;chain.sleep(N)&lt;/code&gt; were inside another expression like &lt;code&gt;chain.sleep(N) + something&lt;/code&gt;, it would be left alone (FN over FP).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. tx-dict to kwargs across multi-line deploy calls
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;strategist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gov&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Wei&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.1 ether&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;strategist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gov&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.1 ether&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&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;Both transforms in one call: tx-dict rewrite (Pass 4) + Wei rewrite (Pass 9) with auto-import of &lt;code&gt;convert&lt;/code&gt;. The whitelist guard verified all keys in the dict (&lt;code&gt;from&lt;/code&gt;, &lt;code&gt;value&lt;/code&gt;) are valid tx-dict keys before firing — if there had been an extra key like &lt;code&gt;priority_fee&lt;/code&gt; (which Brownie supports but Ape doesn't have a direct mapping for), the codemod would have skipped this rewrite to avoid information loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Network detection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show_active&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mainnet-fork&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HIGH_FEE&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LOW_FEE&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mainnet-fork&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HIGH_FEE&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LOW_FEE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass 2b — bare &lt;code&gt;network.show_active()&lt;/code&gt; (without &lt;code&gt;brownie.&lt;/code&gt; prefix). Required because &lt;code&gt;network&lt;/code&gt; was imported directly from brownie.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the codemod surfaced as TODOs
&lt;/h2&gt;

&lt;p&gt;After the codemod, these are the remaining manual items:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"TODO(brownie-to-ape)"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
./conftest.py:8: TODO&lt;span class="o"&gt;(&lt;/span&gt;brownie-to-ape&lt;span class="o"&gt;)&lt;/span&gt;: no direct Ape equivalent &lt;span class="k"&gt;for&lt;/span&gt;: Contract
./conftest.py:15: TODO&lt;span class="o"&gt;(&lt;/span&gt;brownie-to-ape&lt;span class="o"&gt;)&lt;/span&gt;: no direct Ape equivalent &lt;span class="k"&gt;for&lt;/span&gt;: Strategy
./tests/conftest.py:6: TODO&lt;span class="o"&gt;(&lt;/span&gt;brownie-to-ape&lt;span class="o"&gt;)&lt;/span&gt;: no direct Ape equivalent &lt;span class="k"&gt;for&lt;/span&gt;: Vault
./tests/conftest.py:7: TODO&lt;span class="o"&gt;(&lt;/span&gt;brownie-to-ape&lt;span class="o"&gt;)&lt;/span&gt;: no direct Ape equivalent &lt;span class="k"&gt;for&lt;/span&gt;: Token
./tests/test_operation.py:2: TODO&lt;span class="o"&gt;(&lt;/span&gt;brownie-to-ape&lt;span class="o"&gt;)&lt;/span&gt;: preserve_web3 — review whether to migrate to convert&lt;span class="o"&gt;(&lt;/span&gt;...&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5 TODOs total — 4 contract-name flags and 1 web3-preserve note.&lt;/p&gt;

&lt;p&gt;These &lt;strong&gt;cannot be auto-resolved&lt;/strong&gt; without project schema introspection (the jssg sandbox has no filesystem access, so it can't read &lt;code&gt;contracts/*.sol&lt;/code&gt; to find contract names). Each TODO needs a human or AI agent to decide:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Contract&lt;/code&gt; flag&lt;/strong&gt; — decide between &lt;code&gt;ape.Contract(addr)&lt;/code&gt; (loads any address with auto-fetched ABI) or use the existing project artifact pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Strategy&lt;/code&gt;, &lt;code&gt;Vault&lt;/code&gt;, &lt;code&gt;Token&lt;/code&gt; flags&lt;/strong&gt; — these are project-specific contracts. The fix is &lt;code&gt;project.Strategy&lt;/code&gt;, &lt;code&gt;project.Vault&lt;/code&gt;, &lt;code&gt;project.Token&lt;/code&gt;. An AI agent reads the TODO + the surrounding code and applies the right &lt;code&gt;project.&amp;lt;Name&amp;gt;&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;preserve_web3&lt;/code&gt;&lt;/strong&gt; — one test uses &lt;code&gt;web3.eth.get_balance()&lt;/code&gt;. The codemod's Pass 16 flagged this as a TODO because the Ape equivalent depends on whether the user wants &lt;code&gt;networks.provider.web3&lt;/code&gt; (raw web3 access) or &lt;code&gt;chain.provider.get_balance()&lt;/code&gt; (Ape-native).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The AI/manual cleanup pass
&lt;/h2&gt;

&lt;p&gt;Following the codemod's TODO comments, the AI cleanup edits look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- a/conftest.py
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/conftest.py
&lt;/span&gt;&lt;span class="p"&gt;@@ -5,7 +5,7 @@&lt;/span&gt; from ape import accounts, networks, chain, config
&lt;span class="err"&gt;
&lt;/span&gt; @pytest.fixture
&lt;span class="gd"&gt;-# TODO(brownie-to-ape): no direct Ape equivalent for: Strategy
&lt;/span&gt; def strategy(gov, vault, strategist):
&lt;span class="gd"&gt;-    return Strategy.deploy(vault, strategist, sender=gov)
&lt;/span&gt;&lt;span class="gi"&gt;+    return project.Strategy.deploy(vault, strategist, sender=gov)
+
+from ape import project
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;project&lt;/code&gt; import is added once at the top of each file that needs it. Total AI/manual cleanup: &lt;strong&gt;~12 lines across 3 files&lt;/strong&gt; to resolve all 5 TODOs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's different vs token-mix
&lt;/h2&gt;

&lt;p&gt;The token-mix migration (&lt;a href="//./MEDIUM_ARTICLE.md"&gt;companion case study&lt;/a&gt;) is a tutorial repo with simple ERC-20 patterns. Yearn's strategy template is more complex in three specific ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-network config.&lt;/strong&gt; The &lt;code&gt;brownie-config.yaml&lt;/code&gt; has separate entries for &lt;code&gt;mainnet&lt;/code&gt;, &lt;code&gt;mainnet-fork&lt;/code&gt;, &lt;code&gt;polygon-main&lt;/code&gt;, etc. The Python helper &lt;code&gt;migrate_config.py&lt;/code&gt; translates these to &lt;code&gt;ape-config.yaml&lt;/code&gt;'s &lt;code&gt;networks:&lt;/code&gt; block. &lt;strong&gt;Result:&lt;/strong&gt; all 4 network entries translated cleanly with 0 manual edits to the YAML.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Whale impersonation is core, not edge case.&lt;/strong&gt; Tutorial repos rarely test with real holder addresses. Yearn does this for every operation test. The codemod's Pass 7b (&lt;code&gt;accounts.at(addr, force=True)&lt;/code&gt; → &lt;code&gt;impersonate_account&lt;/code&gt;) fired exactly once per test fixture, and the rewrite is semantically correct because both are doing the same thing under the hood: bypass the chain's "is this account unlocked?" check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Contract&lt;/code&gt; flag is more painful.&lt;/strong&gt; Yearn's strategy template uses &lt;code&gt;Contract(VAULT_ADDRESS)&lt;/code&gt; (for already-deployed yearn vaults) AND &lt;code&gt;Strategy.deploy(...)&lt;/code&gt; (for the new strategy). The codemod flags both with the same TODO comment, but the right Ape equivalent differs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Contract(addr)&lt;/code&gt; → &lt;code&gt;ape.Contract(addr)&lt;/code&gt; (this is automatic if &lt;code&gt;from ape import Contract&lt;/code&gt; is added)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Strategy.deploy(...)&lt;/code&gt; → &lt;code&gt;project.Strategy.deploy(...)&lt;/code&gt; (compile-time project artifact)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An AI cleanup agent reading the surrounding code can disambiguate: if the variable name is an address constant (&lt;code&gt;VAULT_ADDRESS = "0x..."&lt;/code&gt;), it's &lt;code&gt;ape.Contract(addr)&lt;/code&gt;; if it's a contract artifact name being deployed (&lt;code&gt;Strategy.deploy(...)&lt;/code&gt;), it's &lt;code&gt;project.Strategy.deploy(...)&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration coverage scorecard
&lt;/h2&gt;

&lt;p&gt;Following the hackathon scoring formula &lt;code&gt;Score = 100 × (1 − ((FP × wFP) + (FN × wFN)) ÷ (N × (wFP + wFN)))&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total patterns visible in the repo (N):&lt;/strong&gt; ~33 across 7 files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-migrated (deterministic):&lt;/strong&gt; ~28 patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TODO-flagged (FN, intentional):&lt;/strong&gt; ~5 patterns (4 contract names + 1 web3 preserve)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incorrect rewrites (FP):&lt;/strong&gt; &lt;strong&gt;0&lt;/strong&gt; — verified by manual diff audit and by running the test suite (post-cleanup)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Auto-coverage: ~85%. Manual-cleanup time: ~5 minutes for someone who's read the codemod's TODO comments and knows Ape's &lt;code&gt;project.X&lt;/code&gt; pattern. AI cleanup: even faster since the TODOs are unambiguous.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this proves
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The codemod scales beyond tutorial repos.&lt;/strong&gt; Yearn's strategy template uses every advanced Brownie idiom (whale impersonation, multi-network config, complex tx-dict shapes, mixed contract types). All of them migrate correctly, with the FN-over-FP design making the remaining manual work trivially identifiable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "why we don't auto-rewrite contract names" rationale holds in production code.&lt;/strong&gt; A codemod that tried to auto-prefix &lt;code&gt;Strategy&lt;/code&gt; with &lt;code&gt;project.&lt;/code&gt; could break repos where &lt;code&gt;Strategy&lt;/code&gt; is imported from elsewhere (a base class, a helper module). Strict scoping requires project schema, which jssg sandboxes deliberately don't have. The TODO approach is the right answer here.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DeFi-specific patterns aren't special.&lt;/strong&gt; The patterns that look unique to DeFi (whale impersonation, multi-network, mainnet forking) all reduce to standard codemod transforms when broken down. The FP-vs-FN tradeoff is the same for tutorial repos and Yearn.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Reproducibility checklist
&lt;/h2&gt;

&lt;p&gt;To verify these results yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/yearn/brownie-strategy-mix.git
&lt;span class="nb"&gt;cd &lt;/span&gt;brownie-strategy-mix

&lt;span class="c"&gt;# Apply codemod&lt;/span&gt;
npx codemod@latest @pugarhuda/brownie-to-ape &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Inspect result&lt;/span&gt;
git diff &lt;span class="nt"&gt;--stat&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"TODO(brownie-to-ape)"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Run config translator&lt;/span&gt;
python /path/to/brownie-to-ape/scripts/migrate_config.py &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output: 4 files changed, ~33 patterns rewritten, 5 TODO comments inserted, 0 incorrect changes.&lt;/p&gt;

&lt;p&gt;For the full 5-repo benchmark including this one, see the &lt;a href="https://github.com/PugarHuda/brownie-to-ape/blob/main/CASE_STUDY.md" rel="noopener noreferrer"&gt;case study repo&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://yearn.finance/" rel="noopener noreferrer"&gt;Yearn Finance&lt;/a&gt; for the open-source strategy template that made this validation possible.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apeworx.io/" rel="noopener noreferrer"&gt;ApeWorx&lt;/a&gt; for the Ape framework and the &lt;a href="https://docs.apeworx.io/ape/latest/userguides/brownie-migration.html" rel="noopener noreferrer"&gt;official Brownie migration guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://codemod.com/" rel="noopener noreferrer"&gt;Codemod&lt;/a&gt; for the jssg engine and the hackathon framing.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.com/" rel="noopener noreferrer"&gt;Anthropic Claude&lt;/a&gt; for collaborative authoring of test fixtures and the AST exploration that informed the FP-guard design.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/PugarHuda/brownie-to-ape" rel="noopener noreferrer"&gt;github.com/PugarHuda/brownie-to-ape&lt;/a&gt; · v0.7.8 · MIT&lt;br&gt;
&lt;strong&gt;Codemod registry:&lt;/strong&gt; &lt;a href="https://app.codemod.com/registry/@pugarhuda/brownie-to-ape" rel="noopener noreferrer"&gt;&lt;code&gt;@pugarhuda/brownie-to-ape&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Related:&lt;/strong&gt; &lt;a href="//./MEDIUM_ARTICLE.md"&gt;token-mix end-to-end case study&lt;/a&gt; · &lt;a href="//./CASE_STUDY_3_TRADEOFFS.md"&gt;engineering tradeoffs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>python</category>
      <category>tutorial</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
