<?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: ethrpg</title>
    <description>The latest articles on DEV Community by ethrpg (@eth-rpg).</description>
    <link>https://dev.to/eth-rpg</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%2F3793051%2F332c333d-8630-4134-bfbb-a05d755b49dd.png</url>
      <title>DEV Community: ethrpg</title>
      <link>https://dev.to/eth-rpg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eth-rpg"/>
    <language>en</language>
    <item>
      <title>I audited my AI coding setup: a security hook that never ran, and 11 dead agents</title>
      <dc:creator>ethrpg</dc:creator>
      <pubDate>Tue, 28 Jul 2026 12:56:32 +0000</pubDate>
      <link>https://dev.to/eth-rpg/i-audited-my-ai-coding-setup-a-security-hook-that-never-ran-and-11-dead-agents-5fk</link>
      <guid>https://dev.to/eth-rpg/i-audited-my-ai-coding-setup-a-security-hook-that-never-ran-and-11-dead-agents-5fk</guid>
      <description>&lt;p&gt;For months I believed my Claude Code setup was fine. Hooks, agents, rules, automations. Everything looked healthy, because nothing threw errors.&lt;/p&gt;

&lt;p&gt;Then I audited the whole thing for the first time since building it. Roughly half of it was either dead or faking it.&lt;/p&gt;

&lt;p&gt;The worst find: a security hook that had never run. Not once. Since the day I installed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The security hook that never fired
&lt;/h2&gt;

&lt;p&gt;I wrote a hook to stop secrets (API keys, tokens) from leaking into tool output. The code checked a field called &lt;code&gt;tool_result&lt;/code&gt;. The actual field in the payload is named &lt;code&gt;tool_response&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One word. The match never succeeded, so the hook silently did nothing on every single call, for months. No error, no warning. A guard that never fires looks exactly like a guard with nothing to catch.&lt;/p&gt;

&lt;p&gt;Fixing the typo would not have been enough, though. The hook ran &lt;em&gt;after&lt;/em&gt; the tool executed. By the time it could mask anything, the model had already read the output, secret included. Post-hoc masking is too late by design, not by accident.&lt;/p&gt;

&lt;p&gt;So I rebuilt it to block &lt;em&gt;before&lt;/em&gt; execution: intercept the tool call, refuse it if it touches sensitive files. Then I fed it 32 test inputs, normal ones and ones with planted secrets, and watched it pass and block correctly. Only after that did I get to say "it works". For the months before, I had been trusting a safety device that did not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eleven agents that were not there
&lt;/h2&gt;

&lt;p&gt;I keep agent definitions as markdown files with YAML frontmatter. At some point I added a one-line comment above the frontmatter in a batch of files. Something like &lt;code&gt;# Part of ...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That single line broke frontmatter parsing. The system stopped recognizing those files as agents.&lt;/p&gt;

&lt;p&gt;Eleven agents, including my architect, planner, and code reviewer, were not registered at all. The definition files sat there looking perfectly fine. No error, no warning. They were simply missing from the roster, and I kept believing I was "using" them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smaller rot
&lt;/h2&gt;

&lt;p&gt;The rest of the audit was the same story in smaller portions.&lt;/p&gt;

&lt;p&gt;A work log had grown to 20MB because a sync script had gone missing. It was being read in full at every session start, burning a second or two each time.&lt;/p&gt;

&lt;p&gt;A hook had &lt;code&gt;timeout: 5000&lt;/code&gt;. The unit was seconds. I had configured an 83-minute timeout.&lt;/p&gt;

&lt;p&gt;A routing table had 23 entries pointing at agents that do not exist in my environment. They were copied from someone else's config and never pruned.&lt;/p&gt;

&lt;p&gt;And one find surprised me: upgrading the model made the rot worse. The new model follows instructions more literally. Pressure phrases I had written to bully the old model ("ALWAYS do X immediately") started causing overtriggering: excessive tool calls, double-checking trivial things. If you swap models and keep your config unchanged, the config drifts out of tune with the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  "It exists" is not "it works"
&lt;/h2&gt;

&lt;p&gt;Every failure above shares one property: silence. A comment line, a field name, a time unit. None of them threw. The system looked healthy, and I kept assuming it was.&lt;/p&gt;

&lt;p&gt;"No errors" is not evidence of "working". Dead configs do not scream. If you never open them up, you never even find the bodies.&lt;/p&gt;

&lt;p&gt;The fix is not sophisticated. Treat config the way you treat code, and audit it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security hooks: feed them planted secrets and watch them actually block.&lt;/li&gt;
&lt;li&gt;Agents: confirm they actually get invoked in a session.&lt;/li&gt;
&lt;li&gt;Anything that publishes or writes: read the result back and verify it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I skipped that last one once. It cost me a batch of wiped post titles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This is one person's setup, audited once. I do not know whether other setups rot at the same rate.&lt;/li&gt;
&lt;li&gt;The audit was manual. I have not settled on a cadence or automation for making it regular.&lt;/li&gt;
&lt;li&gt;The specific failures are mine. Different tools and versions will rot in different places.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is an English adaptation of a post I originally wrote in Korean (June 2026). The incidents and numbers are unchanged. Originally published at &lt;a href="https://cd4761.blogspot.com/2026/07/ai-coding-config-rot.html" rel="noopener noreferrer"&gt;cd4761.blogspot.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a Tool That Turns Any Ethereum Wallet Into an RPG Character — Here's How</title>
      <dc:creator>ethrpg</dc:creator>
      <pubDate>Thu, 26 Feb 2026 00:23:38 +0000</pubDate>
      <link>https://dev.to/eth-rpg/i-built-a-tool-that-turns-any-ethereum-wallet-into-an-rpg-character-heres-how-oid</link>
      <guid>https://dev.to/eth-rpg/i-built-a-tool-that-turns-any-ethereum-wallet-into-an-rpg-character-heres-how-oid</guid>
      <description>&lt;p&gt;What if your Ethereum wallet had a character class? What if every swap, mint, and bridge you've ever done contributed to your RPG stats?&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://ethrpg.app" rel="noopener noreferrer"&gt;Eth·RPG&lt;/a&gt; to answer that question. It reads your on-chain history and generates a full RPG character card — class, stats, combat power, and an AI-written fantasy backstory.&lt;/p&gt;

&lt;p&gt;No wallet connection. No signing. Completely read-only.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.ethrpg.app%2Fapi%2Fog%2F0xd8da6bf26964af9d7eed9e03e53415d37aa96045" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.ethrpg.app%2Fapi%2Fog%2F0xd8da6bf26964af9d7eed9e03e53415d37aa96045" alt="Eth·RPG character card example" width="1200" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Engineering Challenge
&lt;/h2&gt;

&lt;p&gt;Take a dataset with wild variance — one user has 3 transactions, another has 50,000 — and turn it into a balanced, visually shareable result in under 10 seconds. It's like taking everyone's Spotify listening history and generating a D&amp;amp;D character sheet that feels &lt;em&gt;fair&lt;/em&gt; for both casual listeners and power users.&lt;/p&gt;

&lt;p&gt;The core challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Normalization&lt;/strong&gt; — how do you score data that spans 5+ orders of magnitude?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classification&lt;/strong&gt; — how do you categorize users without machine learning?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation&lt;/strong&gt; — how do you make AI output feel personal, not generic?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendering&lt;/strong&gt; — how do you produce shareable images with zero client-side processing?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Fetch On-Chain Data
&lt;/h3&gt;

&lt;p&gt;I use the &lt;a href="https://www.alchemy.com/sdk" rel="noopener noreferrer"&gt;Alchemy SDK&lt;/a&gt; to pull everything about a wallet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ETH balance&lt;/li&gt;
&lt;li&gt;Transaction count&lt;/li&gt;
&lt;li&gt;All asset transfers (ETH, ERC-20, ERC-721, ERC-1155)&lt;/li&gt;
&lt;li&gt;First/last transaction timestamps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two transfer queries (sent + received) are combined to get the full picture. Max 1000 transfers per direction with pagination support.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Classify Transactions
&lt;/h3&gt;

&lt;p&gt;Each transfer is classified against a protocol whitelist — ~40 contract addresses mapped to four categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DEX&lt;/strong&gt;: Uniswap V2/V3, SushiSwap, Curve, 1inch, CoW Protocol...&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NFT&lt;/strong&gt;: Seaport, OpenSea, LooksRare, Blur&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bridge&lt;/strong&gt;: Optimism, Arbitrum, zkSync, Stargate, Hop, Base...&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stablecoin&lt;/strong&gt;: USDC, USDT, DAI, FRAX, GHO...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose hardcoded whitelisting over ABI decoding. It's faster, simpler, and covers 95%+ of real-world activity. One important edge case: utility NFTs (ENS domains, POAPs, Uniswap V3 LP positions) are excluded from the "collectible NFT" count to avoid misclassification.&lt;/p&gt;

&lt;p&gt;The output is a set of ratios:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nftRatio = nftTransfers / totalTransfers
dexRatio = dexSwaps / totalTransfers
bridgeRatio = bridgeTransfers / totalTransfers
stableRatio = stableTransfers / totalTransfers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Determine Class (Priority Matching)
&lt;/h3&gt;

&lt;p&gt;Eight classes, evaluated in order. First match wins:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Hunter&lt;/td&gt;
&lt;td&gt;NFT ratio &amp;gt;= 25% AND nftRatio dominates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Rogue&lt;/td&gt;
&lt;td&gt;DEX swap ratio &amp;gt;= 20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Summoner&lt;/td&gt;
&lt;td&gt;Bridge tx &amp;gt;= 8 or ratio &amp;gt;= 12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Merchant&lt;/td&gt;
&lt;td&gt;Stablecoin ratio &amp;gt;= 25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Priest&lt;/td&gt;
&lt;td&gt;Gas &amp;gt; 1.0 ETH AND contracts &amp;gt; 150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Elder Wizard&lt;/td&gt;
&lt;td&gt;4+ year wallet, &amp;lt; 30 tx/year, &amp;lt; 10 ETH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Guardian&lt;/td&gt;
&lt;td&gt;&amp;lt; 200 txs AND balance &amp;gt; 5 ETH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Warrior&lt;/td&gt;
&lt;td&gt;Default fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The implementation is an array of matcher functions. Adding a 9th class means inserting one function at the right priority position — no nested &lt;code&gt;if/else&lt;/code&gt; chains.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CLASS_MATCHERS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClassMatcher&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;classId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hunter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nftRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nftRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dexRatio&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;classId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rogue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dexRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;classId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summoner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bridgeCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bridgeRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... first match wins&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;determineClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WalletMetrics&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CharacterClassId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;classId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;CLASS_MATCHERS&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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;classId&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warrior&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// default&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Calculate Stats (Log-Scale Normalization)
&lt;/h3&gt;

&lt;p&gt;This is where it gets mathematically interesting.&lt;/p&gt;

&lt;p&gt;On-chain data has an enormous range. Vitalik has mass transactions; a fresh wallet has 3. Linear scaling breaks immediately — whales would max out every stat while most wallets cluster near zero.&lt;/p&gt;

&lt;p&gt;The solution: &lt;strong&gt;logarithmic normalization&lt;/strong&gt;. Think of it like the Richter scale for earthquakes — a magnitude 6 is 10x stronger than a 5, not just "1 more." Same idea here: each order of magnitude in activity adds a fixed increment to the stat.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calcLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;txCount&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="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calcHP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balanceEth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;balanceEth&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calcMP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gasSpentEth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gasSpentEth&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calcSTR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dexSwaps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bridgeCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dexSwaps&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;bridgeCount&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;Why this works: &lt;code&gt;log₁₀(10) = 1&lt;/code&gt;, &lt;code&gt;log₁₀(100) = 2&lt;/code&gt;, &lt;code&gt;log₁₀(10000) = 4&lt;/code&gt;. The function compresses large ranges into tight, meaningful intervals. A wallet with 10 transactions and one with 10,000 both get &lt;em&gt;differentiable, non-extreme&lt;/em&gt; stats.&lt;/p&gt;

&lt;p&gt;The stat mappings are intuitive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ETH balance → HP&lt;/strong&gt; (wealth = health)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gas spent → MP&lt;/strong&gt; (gas is your magical resource)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEX/bridge activity → STR&lt;/strong&gt; (active trading = strength)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique contracts → INT&lt;/strong&gt; (diversity of interaction = intelligence)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction frequency → DEX&lt;/strong&gt; (pace of activity = dexterity)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combat Power combines everything with weighted coefficients plus a class bonus that compensates for classes with naturally lower raw stats.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Generate AI Lore
&lt;/h3&gt;

&lt;p&gt;Each wallet gets a unique fantasy backstory. This was the trickiest part — how do you make AI output feel &lt;em&gt;personal&lt;/em&gt; to a specific wallet, not just generic fantasy text?&lt;/p&gt;

&lt;p&gt;The answer: &lt;strong&gt;a structured prompt with a translation dictionary.&lt;/strong&gt; Real crypto events get mapped to RPG equivalents:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CRYPTO_EVENT_DICTIONARY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Luna/Terra Collapse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The Fall of the Lunar Kingdom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The Merge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The Great Convergence&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NFT Summer 2021&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The Season of Relic Fever&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DeFi Summer 2020&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The Alchemy Renaissance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FTX Collapse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The Merchant Guild Betrayal&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="c1"&gt;// The prompt includes the wallet's class, stats, active period,&lt;/span&gt;
&lt;span class="c1"&gt;// and which events overlap with their transaction history&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  You are writing lore for a &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (Level &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, Power &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;power&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;).
  This hero was active during: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;matchedEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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="s2"&gt;.
  Their primary activity: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dominantBehavior&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.
  Write a 3-5 sentence dark fantasy backstory in second person.
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system has a &lt;strong&gt;4-step fallback cascade&lt;/strong&gt; — because an AI-dependent feature can't be a single point of failure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Primary LLM call (10s timeout with AbortController)&lt;/li&gt;
&lt;li&gt;Fallback LLM model&lt;/li&gt;
&lt;li&gt;Direct Anthropic API call&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deterministic template fallback&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 4 is the interesting one. When all LLM calls fail, a hash function maps &lt;code&gt;classId-level-power-txCount&lt;/code&gt; to a pre-written template:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deterministicHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&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="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;char&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&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="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Same wallet always gets the same fallback — no flickering on retry&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;templateIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deterministicHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;classId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;power&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All generated text passes through a sanitization layer: a forbidden word list catches financial terms or inappropriate language that might leak through the LLM.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Render the Card Image (Server-Side)
&lt;/h3&gt;

&lt;p&gt;Card images are rendered with &lt;code&gt;@vercel/og&lt;/code&gt; (Satori) — JSX components compiled to PNG on the server. No canvas, no ImageMagick, no Puppeteer.&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;// This React component becomes a 1080x1350 PNG&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&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="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ClassHeader&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StatBars&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PowerDisplay&lt;/span&gt; &lt;span class="nx"&gt;power&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;power&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoreText&lt;/span&gt; &lt;span class="nx"&gt;lore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lore&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;,
&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1350&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;Two image formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Card&lt;/strong&gt;: 1080x1350 (mobile feed optimized, shareable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OG&lt;/strong&gt;: 1200x630 (link preview on Twitter/Farcaster)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are CDN-cached for 24 hours. Error states render a visual error card rather than returning an HTTP error — better UX when images are embedded in feeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Fun Results
&lt;/h2&gt;

&lt;p&gt;I tested ~60 wallets. Some highlights:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Wallet&lt;/th&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Power&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vitalik.eth&lt;/td&gt;
&lt;td&gt;Hunter&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;62,045&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stani.eth (Aave)&lt;/td&gt;
&lt;td&gt;Priest&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;62,005&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pranksy.eth&lt;/td&gt;
&lt;td&gt;Hunter&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;70,140&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jessepollak.eth (Base)&lt;/td&gt;
&lt;td&gt;Hunter&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;47,195&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ricmoo.eth (ethers.js)&lt;/td&gt;
&lt;td&gt;Guardian&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;45,150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All 8 classes are represented in the wild. Summoner is the rarest — heavy bridge users are a small subset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; (strict mode)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alchemy SDK&lt;/strong&gt; — on-chain data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude API&lt;/strong&gt; — lore generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@vercel/og (Satori)&lt;/strong&gt; — server-side image rendering&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind CSS 4&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; — deployment + edge caching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentry&lt;/strong&gt; — error monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Protocol whitelisting doesn't scale.&lt;/strong&gt; It works for ~40 known contracts, but any new DEX or NFT marketplace is invisible until manually added. A production system would need transaction decoding via ABI or a classification API like Transpose. I chose the whitelist because it ships in a day; the alternative is a week of parser work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In-memory cache resets on cold starts.&lt;/strong&gt; I launched with a simple &lt;code&gt;Map&amp;lt;string, CachedResult&amp;gt;&lt;/code&gt; and a 24h TTL. First cold start after deploy wiped everyone's cached results. I added Vercel KV (Redis) as a persistence layer on day 3 — should have started there. The lesson: if your cache holds user-visible data, it needs to survive restarts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log-scale tuning is more art than science.&lt;/strong&gt; I tested ~60 known wallets (Vitalik, DeFi founders, NFT collectors, fresh wallets) and manually adjusted multipliers until the stat distribution "felt right." A better approach: collect 1000+ wallet samples, plot the actual distribution, and derive multipliers statistically. I might do this for v2.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ethrpg.app" rel="noopener noreferrer"&gt;https://ethrpg.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Paste any ETH address or ENS name. Takes about 10 seconds.&lt;/p&gt;

&lt;p&gt;Have you ever gamified raw data into something shareable? I'd love to hear what scoring or classification approaches worked for you — or what pitfalls you ran into.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by a solo developer over a 7-day sprint. The entire codebase is Next.js + TypeScript.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>webdev</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
