<?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: tiancaijb</title>
    <description>The latest articles on DEV Community by tiancaijb (@tiancaijb_3df5e4436e5a5fc).</description>
    <link>https://dev.to/tiancaijb_3df5e4436e5a5fc</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%2F4042873%2F2fbe66ba-9491-4e93-94a0-3d502cd58760.png</url>
      <title>DEV Community: tiancaijb</title>
      <link>https://dev.to/tiancaijb_3df5e4436e5a5fc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tiancaijb_3df5e4436e5a5fc"/>
    <language>en</language>
    <item>
      <title>Why I'm preselling my Obsidian AI plugin before writing a line of code</title>
      <dc:creator>tiancaijb</dc:creator>
      <pubDate>Sun, 02 Aug 2026 22:40:18 +0000</pubDate>
      <link>https://dev.to/tiancaijb_3df5e4436e5a5fc/why-im-preselling-my-obsidian-ai-plugin-before-writing-a-line-of-code-1dj9</link>
      <guid>https://dev.to/tiancaijb_3df5e4436e5a5fc/why-im-preselling-my-obsidian-ai-plugin-before-writing-a-line-of-code-1dj9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Disclosure: I used an AI coding agent to collect research and draft this post. I reviewed the final copy before publishing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I started with an Obsidian plugin idea I thought was obvious. Research killed it. A different one survived, and I'm now preselling it at $19 before writing a single line of code.&lt;/p&gt;

&lt;p&gt;Here's the evidence that changed my mind, and the line I've drawn for myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The market has a reputation problem, and it's earned
&lt;/h2&gt;

&lt;p&gt;Obsidian has 1,800+ community plugins and no official way to monetize them. So developers roll their own licensing. That's where things go wrong.&lt;/p&gt;

&lt;p&gt;A real example, from a forum post about two paid AI plugins:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Immediately after payment, I encountered a problem with the license key, which I had to request via email. While seeking help, I noticed many similar posts on the forum, indicating this issue has persisted for over a year."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A year. A paying customer waits for a license key by email, while dozens of other users report the same thing.&lt;/p&gt;

&lt;p&gt;The CEO of Obsidian, Kepano, confirmed the company won't build an official paid-plugin marketplace: "We are explicitly not doing this." Fair enough. But it means every paid plugin author is building their own licensing, their own delivery, their own support.&lt;/p&gt;

&lt;p&gt;Enough of them do it badly that licensing has become a recurring community complaint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other failure mode: ship it, abandon it
&lt;/h2&gt;

&lt;p&gt;Plugins get momentum at launch and then stall. It's the most common complaint I found — "I prefer paying to have a well-maintained and feature-rich plugin rather than having to switch between abandoned ones."&lt;/p&gt;

&lt;p&gt;That is a useful demand signal, not a guarantee. Some people will pay for maintenance, not just features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prices people actually pay
&lt;/h2&gt;

&lt;p&gt;I collected real price points from paid Obsidian plugins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SystemSculpt: $19/month or $149 lifetime&lt;/li&gt;
&lt;li&gt;MeetingMind Pro: $39 lifetime&lt;/li&gt;
&lt;li&gt;Typical Gumroad plugin range: $29-149&lt;/li&gt;
&lt;li&gt;Top-earning Obsidian plugin on Gumroad: ~$4,200 in month one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Individual developers report $300-4,500/month as a realistic single-person range. So the "indie plugin" game is small but real. My target is at the very bottom of that range.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm building, and how the research shaped it
&lt;/h2&gt;

&lt;p&gt;The idea is a plugin that treats your vault as a body of reasoning, not a folder to clean. It is called Vault Coherence: a semantic audit for contradictions, missing context, conclusions that appear to lack supporting notes, and ideas that have drifted apart. The value is a clear review of what needs attention, not another chat window.&lt;/p&gt;

&lt;p&gt;Research changed three decisions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;License delivery must be part of the product.&lt;/strong&gt; At release, payment should lead to an immediate key, not an email wait. That alone differentiates from the most established competitor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local-first by default.&lt;/strong&gt; Bring your own API key (DeepSeek, OpenAI, or Ollama locally). Obsidian's developer policy requires disclosing outbound requests; we disclose them in settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance is a promise, not a footnote.&lt;/strong&gt; Public roadmap, twelve months of updates, build-in-public from day one.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why presell
&lt;/h2&gt;

&lt;p&gt;I've been here before. My previous Obsidian plugin — obsidian-gtd, a task-management tool — hit 500 downloads and made exactly $0. It worked. People downloaded it, some used it daily. Nobody paid. A free plugin quietly teaches people your tool is worth nothing.&lt;/p&gt;

&lt;p&gt;So this time the money comes first, or the plugin doesn't get built.&lt;/p&gt;

&lt;p&gt;Because opinions are cheap and purchases are not. The Mom Test has a useful rule: talk about people's lives, not your idea; ask about the past, not the future. A landing page where strangers either pay or don't is the most honest interview there is.&lt;/p&gt;

&lt;p&gt;The bar is deliberately low: two sales and I build. Four weeks with zero and I switch to the backup idea. Either outcome is data I can act on.&lt;/p&gt;

&lt;p&gt;The presale price is $19 for the first 20 buyers (planned final price: $39). Development starts after the second presale, with delivery within two weeks after that milestone. If the second presale does not arrive within four weeks of launch, everyone gets a full refund. You can also request a refund if I miss delivery or the plugin is not right for you.&lt;/p&gt;

&lt;p&gt;If this sounds like the tool your vault needs, here's the page. If it doesn't, tell me what you actually use for this — that's also data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tiancai8.gumroad.com/l/vault-doctor" rel="noopener noreferrer"&gt;Vault Coherence — $19 presale&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the presale clears that bar, I'll post the build in public here. I plan to start with the licensing system, since that's where everyone else seems to stumble.&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>indiehackers</category>
      <category>buildinpublic</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a dev.to publishing tool for my coding agent. The key problem was the shell, not the code.</title>
      <dc:creator>tiancaijb</dc:creator>
      <pubDate>Sun, 02 Aug 2026 15:09:33 +0000</pubDate>
      <link>https://dev.to/tiancaijb_3df5e4436e5a5fc/i-built-a-devto-publishing-tool-for-my-coding-agent-the-key-problem-was-the-shell-not-the-code-5ca9</link>
      <guid>https://dev.to/tiancaijb_3df5e4436e5a5fc/i-built-a-devto-publishing-tool-for-my-coding-agent-the-key-problem-was-the-shell-not-the-code-5ca9</guid>
      <description>&lt;p&gt;My coding agent runs most of my writing workflow now. Drafts live as markdown files, and publishing them to dev.to was the one manual step left. So I made it a tool the agent can call itself. One 8.9KB extension file. Three actions. Two environment traps that had nothing to do with the code.&lt;/p&gt;

&lt;p&gt;This is part of a build-in-public experiment that started in August 2026. I ran 43 sites to almost $0 and ~320 monthly visits before realizing the lesson: dev.to gives more traction in 48 hours than 25 static sites got in a month. Publishing fast and often is the whole game. The publish step has to be cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://github.com/earendil-works/pi" rel="noopener noreferrer"&gt;pi&lt;/a&gt; coding agent (any agent with an extension system works, the pattern carries over)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.pi/agent/extensions/&lt;/code&gt; as the extension directory&lt;/li&gt;
&lt;li&gt;A dev.to API key, generated in your dev.to settings&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;pi auto-discovers TypeScript files in &lt;code&gt;~/.pi/agent/extensions/&lt;/code&gt; and loads them with jiti, no build step. An extension is just a factory function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;typebox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StringEnum&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@earendil-works/pi-ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ExtensionAPI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@earendil-works/pi-coding-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExtensionAPI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;publish_devto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Publish or update markdown drafts to dev.to, or list status.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StringEnum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&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="s2"&gt;create&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="s2"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
      &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_toolCallId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool exposes three actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;list&lt;/strong&gt; — show what's in &lt;code&gt;drafts/&lt;/code&gt; and &lt;code&gt;published/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;create&lt;/strong&gt; — read a draft, POST it to dev.to as a &lt;em&gt;draft&lt;/em&gt; by default, write back &lt;code&gt;devto_id&lt;/code&gt;/&lt;code&gt;devto_url&lt;/code&gt; into the frontmatter, move the file to &lt;code&gt;published/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;update&lt;/strong&gt; — PUT changes to an existing article using the stored &lt;code&gt;devto_id&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design choice that matters: the markdown file is the source of truth. Frontmatter holds the title and the dev.to IDs. What you see in the file is what goes to dev.to, minus the bookkeeping keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API key trap
&lt;/h2&gt;

&lt;p&gt;The first version read the key from the shell environment. It worked in my terminal. It failed inside the agent session. The agent is launched by a terminal multiplexer, whose bash panes are &lt;em&gt;non-interactive&lt;/em&gt; shells. My &lt;code&gt;.bashrc&lt;/code&gt; has this as its sixth line:&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="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$-&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;i&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a common early-exit guard: "if this shell isn't interactive, stop reading." Everything after it — including my &lt;code&gt;export DEVTO_API_KEY=...&lt;/code&gt; — never runs in the agent's shell. The key simply wasn't there.&lt;/p&gt;

&lt;p&gt;The fix was to stop trusting the environment entirely:&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;devtoApiKey&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromEnv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEVTO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fromEnv&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;fromEnv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;KEY_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Env var first, then a fallback to &lt;code&gt;~/.pi/agent/devto.key&lt;/code&gt; with &lt;code&gt;chmod 600&lt;/code&gt;. The key never lives in code, and the tool works whether or not the shell bothers to load my exports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more traps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;401 that wasn't the key.&lt;/strong&gt; &lt;code&gt;GET /api/articles/me&lt;/code&gt; without a &lt;code&gt;?state=&lt;/code&gt; parameter returns 401 even with a valid key. That's the endpoint's behavior, not a credential problem. Use &lt;code&gt;?state=published&lt;/code&gt; or just test with a POST.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The write-back bug.&lt;/strong&gt; &lt;code&gt;create&lt;/code&gt; writes &lt;code&gt;devto_id&lt;/code&gt; and &lt;code&gt;devto_url&lt;/code&gt; back into the frontmatter of the archived file. My first pass got that bookkeeping wrong. A mock-API end-to-end test caught it before anything touched the real endpoint — worth the ten minutes to set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow it enables
&lt;/h2&gt;

&lt;p&gt;The agent now drafts, I review the file, &lt;code&gt;publish_devto create&lt;/code&gt; stores it as a dev.to draft, and I hit publish in the browser. One human gate on the final click, everything else scripted.&lt;/p&gt;

&lt;p&gt;The project is a public experiment in whether a solo developer can get to $1/day with a small agent budget. The machine that produces the content is also the content. This tool is one more brick, and the whole thing is on &lt;a href="https://github.com/tiancaijb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One more thing: this is the first extension the agent wrote entirely by itself. I gave the machine an evolution loop — it gathers skills from GitHub, writes its own extensions, and I review plus reload. This tool is the first output of that loop.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>piagent</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built 43 sites and made $0. Now I'm trying the opposite approach.</title>
      <dc:creator>tiancaijb</dc:creator>
      <pubDate>Thu, 23 Jul 2026 03:31:47 +0000</pubDate>
      <link>https://dev.to/tiancaijb_3df5e4436e5a5fc/i-built-43-sites-and-made-0-now-im-trying-the-opposite-approach-1g0l</link>
      <guid>https://dev.to/tiancaijb_3df5e4436e5a5fc/i-built-43-sites-and-made-0-now-im-trying-the-opposite-approach-1g0l</guid>
      <description>&lt;h1&gt;
  
  
  I built 25 websites to make $1 each. Here's what happened.
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; 25 single-page cheatsheets. $1 each in crypto. $0 revenue. 15 open PRs. Dev.to drives more traffic than Google. Here's the real math.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The idea
&lt;/h2&gt;

&lt;p&gt;Six weeks ago I had a simple theory:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build single-page cheatsheets for developer tools. Charge $1 in crypto. Let SEO do the work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The pitch writes itself: "I could Google this for free, but $1 saves me 20 minutes of searching."&lt;/p&gt;

&lt;p&gt;It's the classic convenience play. Gumroad proved people pay for well-packaged free information. I just wanted to test if the same works for dev tools, paid in Solana/USDC.&lt;/p&gt;

&lt;p&gt;No subscription. No account. No newsletter upsell. One page, one dollar, one payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What I built
&lt;/h2&gt;

&lt;p&gt;I picked 25 topics across a few categories and built a single-page HTML site for each one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crypto dev tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solana CLI cheatsheet&lt;/li&gt;
&lt;li&gt;Anchor framework quick reference&lt;/li&gt;
&lt;li&gt;Rust CLI essentials&lt;/li&gt;
&lt;li&gt;Web3.py snippets&lt;/li&gt;
&lt;li&gt;Hardhat command reference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;General dev:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bash one-liners (the ones you actually use)&lt;/li&gt;
&lt;li&gt;tmux survival guide&lt;/li&gt;
&lt;li&gt;Git undo commands (the most common page)&lt;/li&gt;
&lt;li&gt;sed/awk patterns&lt;/li&gt;
&lt;li&gt;jq JSON query reference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI coding agents:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code quick commands&lt;/li&gt;
&lt;li&gt;Cursor shortcuts&lt;/li&gt;
&lt;li&gt;Aider workflow reference&lt;/li&gt;
&lt;li&gt;Prompt patterns for codegen&lt;/li&gt;
&lt;li&gt;Vibe coding anti-patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Exchange comparisons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binance vs Bybit fee tier comparison&lt;/li&gt;
&lt;li&gt;Solana DEX aggregator fees&lt;/li&gt;
&lt;li&gt;CEX withdrawal fee reference&lt;/li&gt;
&lt;li&gt;Perpetual exchange comparison matrix&lt;/li&gt;
&lt;li&gt;KYC-free exchange roundup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every site is a single &lt;code&gt;index.html&lt;/code&gt; — no framework, no build step, no JavaScript bloat. Just a clean reference, a buy button (Solana Pay / USDC address), and a permissive license so people can fork it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sites live&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total build time&lt;/td&gt;
&lt;td&gt;~40 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open PRs to Awesome lists&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly visitors (all sites)&lt;/td&gt;
&lt;td&gt;~320&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That zero stings. I'll be honest — I expected at least a couple of sales by now, even if it was friends throwing a dollar to be nice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Costs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domains: ~$90/yr (25 × $3.99 .xyz domains)&lt;/li&gt;
&lt;li&gt;Hosting: $0 (all static, all on Vercel free tier)&lt;/li&gt;
&lt;li&gt;Total sunk: ~$90&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. What worked
&lt;/h2&gt;

&lt;p&gt;Three things actually moved the needle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dev.to articles.&lt;/strong&gt; My post about the project itself got more views in 48 hours than all 25 sites combined got in a month. Devs are on dev.to. They search dev.to. Write there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Great READMEs.&lt;/strong&gt; The GitHub repos with solid READMEs and screenshots get starred more than the sites get visited. Devs window-shop on GitHub, they don't land on raw HTML pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Niche specificity.&lt;/strong&gt; The most visited page is "Git undo commands" — a topic so specific people search it exactly. Broad topics get buried. Narrow pages get found.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. What didn't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Awesome list PRs are glacial.&lt;/strong&gt; 15 open PRs, zero merged. Many have sat for 3+ weeks. The maintainers are volunteers and that's fine, but if your growth strategy depends on landing in &lt;code&gt;awesome-whatever&lt;/code&gt;, you need a different timeline expectation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crypto-only payment is a filter.&lt;/strong&gt; Even if someone wants the cheatsheet, sending $1 in Solana requires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Having an exchange account with SOL in it&lt;/li&gt;
&lt;li&gt;Not minding the $0.01+ transaction fee being 1% of the purchase&lt;/li&gt;
&lt;li&gt;Bothering to do it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's three barriers before the first page load. A $1 Stripe link would convert better, but Stripe isn't available in my region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO takes months.&lt;/strong&gt; I knew this going in, but the reality still hits. 25 sites, 6 weeks, zero organic rankings for anything competitive. The long-tail queries get a trickle, but trickles don't make rent.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The real insight
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;People pay for convenience, not information. &lt;strong&gt;But crypto convenience isn't here yet.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The cheatsheet model isn't broken. Gumroad proved it works — for $3-5 PDFs, with Apple Pay. The friction of sending $1 in SOL for a single page is simply higher than the value it returns.&lt;/p&gt;

&lt;p&gt;What I'd do differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One domain, 25 routes.&lt;/strong&gt; Don't buy 25 domains. Use &lt;code&gt;cheatsheets.example.com/git-undo&lt;/code&gt;. Save $90/yr.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF + Gumroad.&lt;/strong&gt; Let Gumroad handle payment friction. They support crypto too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B2B over B2D.&lt;/strong&gt; Devs are cheap. Sell the package of 25 cheatsheets to teams for $20.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus on one niche.&lt;/strong&gt; 25 topics in 5 categories is too spread. Pick one (AI coding agents, crypto dev tools) and own it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Side hustles for devs
&lt;/h2&gt;

&lt;p&gt;This experiment is part of a bigger collection I'm building — a handbook of developer side hustles that actually work (and the ones that don't). Each chapter is one experiment, one landing page, one set of real numbers.&lt;/p&gt;

&lt;p&gt;I'm documenting everything transparently because most "side hustle" content is survivorship bias wrapped in an affiliate link. I want the real numbers — success, failure, and the 90% that sits in the messy middle.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📖 The side hustle handbook (all experiments): &lt;a href="https://side-hustle-handbook.vercel.app" rel="noopener noreferrer"&gt;side-hustle-handbook.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 GitHub repo (code + experiments): &lt;a href="https://github.com/tiancaijb366-pixel/awesome-developer-side-hustles" rel="noopener noreferrer"&gt;github.com/tiancaijb366-pixel/awesome-developer-side-hustles&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you're building something similar or have ideas to test, DM me. I'm always down to trade real numbers for real feedback.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>startup</category>
      <category>sidehustle</category>
      <category>entrepreneur</category>
      <category>indiemakers</category>
    </item>
    <item>
      <title>Foundry Fuzz &amp; Invariant Testing: A Practical Cookbook</title>
      <dc:creator>tiancaijb</dc:creator>
      <pubDate>Thu, 23 Jul 2026 02:03:08 +0000</pubDate>
      <link>https://dev.to/tiancaijb_3df5e4436e5a5fc/foundry-fuzz-invariant-testing-a-practical-cookbook-4oof</link>
      <guid>https://dev.to/tiancaijb_3df5e4436e5a5fc/foundry-fuzz-invariant-testing-a-practical-cookbook-4oof</guid>
      <description>&lt;p&gt;Most hacks happen in edge cases the dev never considered. Fuzz testing throws random inputs at your contracts until something breaks — and Foundry makes it fast enough that you run it before every commit. Here are the patterns I actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;forge &lt;span class="nb"&gt;install &lt;/span&gt;tiancaijb366-pixel/foundry-security-tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The companion repo has all these examples runnable. Fork it, run &lt;code&gt;forge test&lt;/code&gt;, then rip out the patterns for your own contracts.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Basic Fuzzing with &lt;code&gt;bound&lt;/code&gt; / &lt;code&gt;vm.assume&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Don't write ten &lt;code&gt;test_RevertIf_*&lt;/code&gt; functions. Give the fuzzer a range and let it find the off-by-one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {Token} from "../src/Token.sol";

contract TokenFuzzTest is Test {
    Token t;

    function setUp() public {
        t = new Token(1_000_000e18);
    }

    function testFuzz_Transfer_Bounds(address sender, address to, uint256 amount) public {
        // `bound` keeps inputs practical
        amount = bound(amount, 1, t.balanceOf(sender));

        // `vm.assume` filters — use it sparingly (slows the fuzzer)
        vm.assume(sender != address(0) &amp;amp;&amp;amp; to != address(0) &amp;amp;&amp;amp; sender != to);

        vm.prank(sender);
        t.transfer(to, amount);

        assertGe(t.balanceOf(sender), 0);
        assertEq(t.totalSupply(), 1_000_000e18);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key point:&lt;/strong&gt; &lt;code&gt;bound&lt;/code&gt; is faster than &lt;code&gt;vm.assume&lt;/code&gt; — it narrows the range instead of discarding inputs. Use &lt;code&gt;assume&lt;/code&gt; only for relationships the fuzzer can't infer (e.g., &lt;code&gt;sender != to&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Invariant Testing: ERC20 &lt;code&gt;totalSupply&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Invariant tests check a property holds across random sequences of calls. They catch state-machine bugs that single-function fuzzing misses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
import {Token} from "../src/Token.sol";

contract TokenInvariantTest is StdInvariant, Test {
    Token t;

    function setUp() public {
        t = new Token(1_000_000e18);
        targetContract(address(t));
    }

    // This should _always_ be true
    function invariant_totalSupply() public {
        assertEq(t.totalSupply(), 1_000_000e18);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;forge &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--match-test&lt;/span&gt; invariant &lt;span class="nt"&gt;-vvv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;--fuzz-runs 50000&lt;/code&gt; for serious fuzzing. On a commodity laptop that runs in ~30s and catches things you'd never write a unit test for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ghost variable pattern:&lt;/strong&gt; When the invariant involves contract state over time, track a ghost variable in the test contract that mirrors expected state after every handler call.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Reentrancy Detection via Fuzz
&lt;/h2&gt;

&lt;p&gt;Foundry rolls back state after every fuzz run, so you can brute-force reentrancy paths without setting up a separate exploit contract for each scenario.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {Vault} from "../src/Vault.sol";

contract ReentrancyFuzzTest is Test {
    Vault v;

    // Track the reentrancy attempt
    bool public attackAttempted;
    uint256 public balanceBefore;

    receive() external payable {
        if (attackAttempted) return; // only re-enter once
        attackAttempted = true;

        // Try to drain before the first call finishes
        v.withdraw(balanceBefore);
    }

    function testFuzz_Reentrancy(uint256 depositAmount) public {
        depositAmount = bound(depositAmount, 1 ether, 100 ether);

        // Fund victim
        v.deposit{value: depositAmount}();
        balanceBefore = depositAmount;

        // Attack from this contract
        attackAttempted = false;
        v.withdraw(depositAmount);

        // If reentrancy worked, vault balance would be &amp;lt; 0
        assertLe(address(v).balance, depositAmount);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt; Foundry isolates each fuzz run. You don't need &lt;code&gt;expectRevert&lt;/code&gt; — just check the final state. If &lt;code&gt;totalSupply&lt;/code&gt; or &lt;code&gt;balance&lt;/code&gt; diverged, the fuzzer found a path.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Access Control Fuzzing
&lt;/h2&gt;

&lt;p&gt;The most common finding in real audits: an &lt;code&gt;onlyOwner&lt;/code&gt; modifier that doesn't cover all state-changing paths. Fuzz every function from every caller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {Vault} from "../src/Vault.sol";

contract AccessControlFuzzTest is Test {
    Vault v;

    address owner = makeAddr("owner");
    address attacker = makeAddr("attacker");

    function setUp() public {
        vm.prank(owner);
        v = new Vault();

        deal(attacker, 100 ether);
    }

    // Fuzz every state-changing function as an attacker
    function testFuzz_AccessControl_Withdraw(uint256 amount) public {
        vm.assume(amount &amp;gt; 0 &amp;amp;&amp;amp; amount &amp;lt;= 100 ether);

        vm.prank(attacker);
        vm.expectRevert(); // should always revert for non-owner
        v.emergencyWithdraw(amount);
    }

    function testFuzz_AccessControl_Pause(bool paused) public {
        vm.prank(attacker);
        vm.expectRevert();
        v.setPaused(paused);
    }

    function testFuzz_AccessControl_Mint(uint256 amount) public {
        amount = bound(amount, 0, 1_000_000e18);

        vm.prank(attacker);
        vm.expectRevert();
        v.mint(attacker, amount);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Build a &lt;code&gt;handler&lt;/code&gt; contract that wraps every permissioned function and call it from both privileged and unprivileged addresses in your invariant test suite. One function per access level.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Oracle Manipulation Fuzzing
&lt;/h2&gt;

&lt;p&gt;DeFi exploits almost always involve price oracles returning manipulated values. Fuzz the oracle input and check what happens to your core accounting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {LendingPool} from "../src/LendingPool.sol";
import {MockOracle} from "../src/MockOracle.sol";

contract OracleManipulationFuzzTest is Test {
    LendingPool pool;
    MockOracle oracle;

    address user = makeAddr("user");

    function setUp() public {
        oracle = new MockOracle(1000e8); // ETH/USD = $1000
        pool = new LendingPool(address(oracle));
        deal(user, 100 ether);
    }

    // What happens if the oracle price swings wildly?
    function testFuzz_OracleManipulation(uint256 manipulatedPrice, uint256 collateral) public {
        collateral = bound(collateral, 1 ether, 50 ether);
        manipulatedPrice = bound(manipulatedPrice, 1e8, 100_000e8); // $1 to $100k

        // User deposits collateral
        vm.prank(user);
        pool.deposit{value: collateral}();

        // Oracle is manipulated (flash loan, sandwich, etc.)
        oracle.setPrice(manipulatedPrice);

        // User borrows against inflated collateral — or gets liquidated unfairly
        vm.prank(user);
        pool.borrow();

        // Check: can the pool cover all deposits?
        assertGe(address(pool).balance, pool.totalDeposits());
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this catches:&lt;/strong&gt; If your &lt;code&gt;borrow()&lt;/code&gt; or &lt;code&gt;liquidation&lt;/code&gt; math assumes prices stay within 5% of the previous value, the fuzzer will find the exact value that breaks it. Then you add a circuit breaker or TWAP window.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the Full Suite
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Standard fuzz (default 256 runs per test)&lt;/span&gt;
forge &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;# Heavy fuzz (closer to what auditors run)&lt;/span&gt;
forge &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--fuzz-runs&lt;/span&gt; 50000 &lt;span class="nt"&gt;--ffi&lt;/span&gt;

&lt;span class="c"&gt;# Invariant tests (sequences of calls)&lt;/span&gt;
forge &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--match-test&lt;/span&gt; invariant &lt;span class="nt"&gt;--fuzz-runs&lt;/span&gt; 50000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On CI, run the light suite on every push and the heavy suite nightly.&lt;/p&gt;




&lt;p&gt;Useful? Check out these resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://foundry-cheatsheet.vercel.app" rel="noopener noreferrer"&gt;Foundry Cheatsheet&lt;/a&gt; — every forge command, cheatcode, and flag I always forget&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://contract-audit-checklist.vercel.app" rel="noopener noreferrer"&gt;Contract Audit Checklist&lt;/a&gt; — what to check before any deployment&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://solidity-snippets.vercel.app" rel="noopener noreferrer"&gt;Solidity Snippets&lt;/a&gt; — copy-paste patterns for common patterns&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>crypto</category>
      <category>security</category>
      <category>testing</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
