DEV Community

Cover image for I Spent $200 in One Day on AI Tokens to Build 86 Browser Tools — A Solo Founder's Complete Build Log
Zihang Dong 董子航
Zihang Dong 董子航

Posted on

I Spent $200 in One Day on AI Tokens to Build 86 Browser Tools — A Solo Founder's Complete Build Log

86 tools. Zero backend. One person. Two hospital visits. $200 in tokens in 24 hours. This is what building in public actually looks like — not the highlight reel, but the raw commit history of a human being.


The Genesis: Why Would Anyone Build 86 Free Tools?

On March 18, 2026, I launched ToolKnit — a collection of free, browser-based utilities that require no signup, no uploads, and zero trust.

Today, it's 86 tools across 10 categories. PDF converters that run entirely client-side using PDF-lib.js. Image resizers powered by HTML5 Canvas. A Morse Code Translator with audible beep playback at adjustable WPM. A MIDI Keyboard with 8 synthesized timbres — Grand Piano, Electric Organ, Synth Lead, String Ensemble, Pipa, Guitar, Flute, and Music Box — all generated in real-time through the Web Audio API. A Lyric Visualizer that parses LRC files and syncs lyrics to audio waveforms. Even a drinking-game dare spinner with 300 dares across 6 categories, slot-machine text reveal animation, and confetti celebrations.

But here's what the landing page doesn't tell you: I just spent $200 in a single day on Claude Max tokens to migrate a handful of pages to a new bilingual architecture. I killed a feature after 48 hours of intensive debugging. And somewhere between the ffmpeg.wasm Worker CORS errors and the hospital visit, I learned that some bugs aren't in the code at all.

This isn't a success story. It's a build log.


Part I: The Stack — Why I Chose Boring Technology

Before I tell you about the features that lived and died, you need to understand the foundation. ToolKnit is pure static HTML + Tailwind CSS + Vanilla JS. No React. No Vue. No Next.js. No build step beyond Tailwind compilation.

Why?

  • SEO-first: Static HTML means Google sees everything immediately. No hydration delays. No client-side routing. Every tool page is a complete, crawlable document with full Open Graph tags, Twitter Cards, and structured data.
  • Zero JS framework bloat: Most tools work without JavaScript. Progressive enhancement is the default, not an afterthought.
  • Server costs near zero: The only PHP endpoints are track.php (stats) and public-stats.php (aggregated metrics). All image processing, PDF manipulation, audio synthesis, and video transcoding happens in the browser.
  • Deployment is scp: One file, one command, zero CI/CD complexity. The Service Worker (toolknit-v119 as of today) handles offline caching for returning visitors.

The tradeoff? Manual consistency across 86 pages. When you add a new footer link, you're editing 86 HTML files (or writing a Python script to do it, then verifying each one). That's why the bilingual architecture had to be a shared module, not a framework.

The PWA layer deserves its own mention. manifest.json declares the site as installable. The Service Worker precaches core assets. Users on slow connections get instant loads after the first visit. For a site with 86 tool pages, this matters more than any SPA transition animation.


Part II: The Tools That Lived — Engineering Notes from the Trenches

The MIDI Keyboard: 8 Timbres, 1 Web Audio Context

Building a browser-based MIDI keyboard sounds straightforward until you realize that "piano sound" isn't a single oscillator. Each timbre required distinct synthesis architecture:

Pipa (琵琶): Sawtooth + square wave generators with non-integer harmonics (5.04×, 7.01× fundamental) to simulate the metallic twang of plucked silk strings. A high-Q resonant lowpass filter shapes the body resonance, and a noise burst layer provides the finger-pick attack transient. Without that noise burst, it sounds like a cheap synth preset.

Guitar: Triangle wave fundamental + 6 overtone harmonics with body resonance simulation via 105 Hz and 215 Hz bandpass filters (acoustic cavity modes). The pluck noise layer is subtler than Pipa but essential — without it, notes sound like a keyboard patch, not a string.

Flute: Pure sine wave with faint 2nd harmonic, vibrato LFO at 5.5 Hz (8 cents depth, delayed onset at 0.3s to avoid startup wobble), and breath noise via highpass-filtered white noise at 3 kHz. The breath noise is what separates a "flute-like" synth from an actual flute.

Music Box: Non-integer bell partials (2.756×, 5.404×, 8.933× fundamental) for the metallic chime character, octave-above overtone, and a 400 Hz highpass filter to remove low-end rumble. Long natural decay — no ADSR envelope can fake this without custom release curves.

I also added a Quick Play Songs panel with Für Elise, Summer (Kikujiro), Ode to Joy, Canon in D, Castle in the Sky, and Always With Me. The keyboard auto-shifts octaves as you follow the key sequences. When I first tested it by playing Für Elise with the Z key, I couldn't stop grinning.

The Morse Code Translator: Audio, Flash, and International Alphabet

The Morse Code Translator was the first tool built entirely on the new bilingual blueprint. Two-way text↔Morse with adjustable WPM (5–40), audible beep playback via Web Audio API oscillator nodes, screen-flash signaling for accessibility, and the full international Morse alphabet (including accented characters and Prosigns like SOS, AR, SK).

The audio timing follows strict ITU-R standards: a "dit" is one time unit, a "dah" is three units, intra-character spacing is one unit, inter-character spacing is three units, and inter-word spacing is seven units. Getting the rhythm wrong makes Morse feel "mechanical" rather than "musical." The gap between characters matters as much as the beep itself.

The Lyric Visualizer: Web Audio API + Canvas + LRC Parsing

Shipped the same day I killed Live Photo Frame. Upload an MP3/WAV + LRC lyrics file for a full-screen immersive music player with real-time audio waveform visualization, time-synced fading lyrics, draggable progress bar, automatic ID3 cover art extraction, and a dark/light theme toggle.

The Web Audio API analyzer node pulls frequency data for the waveform canvas. LRC timestamp parsing converts [mm:ss.xx] markers into millisecond anchors. Lyrics fade in/out based on proximity to the current playback position. All in the browser. No server ever sees your audio files.

Spin the Dare: From Canvas Wheel to Slot Machine

The original Spin the Dare used a canvas roulette wheel. I rebuilt it with a slot-machine-style text reveal animation — the dare scrambles through random prompts rapidly, then eases to a dramatic stop with sound cues (scale transitions, pitch-shifted chimes) and confetti explosions.

Why replace a working wheel? Because the wheel had 300 segments at small radii — visually illegible. The slot-machine approach keeps every dare readable at any screen size, adds suspense through the scramble phase, and uses audio cues (pitch rising during scramble, resolving chord at stop) to create a more theatrical experience.


Part III: The Features That Died — When Technical Soundness Isn't Enough

Live Photo Frame: 48 Hours, 3 Fatal Flaws

The idea was elegant: frame iPhone Live Photos (HEIC + MOV pairs) and Android Motion Photos (JPG with embedded MP4) in 9:16 or 16:9 layouts with large rounded corners, black or white backgrounds, and export to static frame or video.

What I built:

  • Client-side HEIC-to-JPEG conversion via heic2any
  • ffmpeg.wasm (single-thread, ~32 MB one-time download) for H.265 MOV transcoding and MP4 encoding
  • Auto-detection of iPhone Live Photo pairs, Android Motion Photos, and standalone video files
  • Canvas rendering with configurable corner radii

Why I killed it after 48 hours:

  1. Canvas quadraticCurveTo draws parabolas, not circular arcs. At radii above ~60px, "rounded corners" morph into chamfered edges. The visual signature of the tool — those elegant large-radius corners — was mathematically impossible with the Canvas API. You need arc() with proper tangent calculations, not quadraticCurveTo().

  2. HEVC/H.265 videos from Android Motion Photos won't play natively in Chrome. ffmpeg.wasm transcoding at 1080×1920 resolution was agonizingly slow — 30+ seconds for a 3-second clip on a MacBook Pro. Users don't wait 30 seconds.

  3. WeChat and messaging apps strip embedded video data from Motion Photos. The primary sharing pipeline (user receives Motion Photo via WeChat → opens in ToolKnit) was destroyed by app-level data stripping. The tool's core use case didn't exist in the real world.

Three strikes: visual quality, performance, and reliability. No amount of patching fixes a fundamentally flawed concept for the browser platform.

"A tool that 'almost works' is worse than no tool at all — it erodes trust. Ship what you're proud of, not what merely functions."

AI Pixel Art GIF: Twice Bitten

First attempt (June 9): Used DeepSeek API to generate pixel art from text prompts. Quality was garbage — LLMs don't understand pixel-level constraints.

Second attempt: Replaced DeepSeek with local Canvas pixelation (drawImage at reduced size, then drawImage back up with imageSmoothingEnabled = false) + gif.js encoding + local color analysis for smart animation selection. Fixed Worker CORS by hosting gif.worker.js locally. Fixed Canvas willReadFrequently warnings. Replaced DeepSeek API 400 errors with local analysis.

All solid engineering fixes. The tool "worked." But the output wasn't compelling. Pixel art GIFs demand either perfect algorithmic control or genuine artistic AI — we had neither.


Part IV: The $200 Day — Rebuilding 19 Pages in Bilingual Architecture

The Problem

Every tool page was English-only. Hardcoded strings everywhere. No SEO structure beyond basic meta tags. Footer layouts inconsistent across 80+ pages. For a site serving both English and Chinese speakers, this was a growth ceiling.

The Architecture

I designed a two-layer bilingual system:

Layer File Scope
Shared UI bilingual.js Nav, footer, breadcrumb, related tools, modals, feature pills
Page-specific [tool].js Hero title/desc, tool UI labels, How It Works, Why Use, Pro Tips, FAQ
Translations locales/zh.json Tool card titles/descriptions for Related Tools
Schemas Inline JSON-LD SoftwareApplication + FAQPage + BreadcrumbList per page

Key decisions:

  • English stays the default. lang="en" is static. SEO is entirely English-first. Chinese is a UX enhancement that persists across sessions via localStorage.
  • Zero impact on legacy pages. bilingual.js only activates on pages that explicitly reference it. Old pages continue working exactly as before.
  • Structured data is mandatory. Every bilingual page ships with FAQPage JSON-LD that matches the visible FAQ accordion content exactly — question-for-question, answer-for-answer.
  • Cross-page language memory. Switch to Chinese on the Image Resizer, visit the PDF Compressor, it remembers your preference.

The Cost

Migrating 5 PDF tools + Spin the Dare + Morse Code Translator + Mic & Camera Test + 11 image converters burned through $100+ in AI tokens. The full June 12 session — which included writing the bilingual.js module, creating the template, and migrating the first batch — hit $200.

The architecture itself is genuinely beautiful. But every refinement comes with a price tag measured in dollars per hour. The remaining 67 legacy pages will migrate gradually. It isn't laziness; it's arithmetic.


Part V: The Migration War — 11 Image Tools, 11 Ways to Break

Over the past 72 hours, I migrated 11 image tools: Image to PDF, Compress Image, JPG to PNG, PNG to JPG, JPG to WebP, PNG to WebP, WebP to JPG, WebP to PNG, Image Grid Split, Image Crop, and Image Resizer.

Each migration follows the same pattern — and produces its own unique bug.

Bug 1: The Single Quote That Killed a Language Switcher

// Inside RESIZER_I18N.en:
why2Desc: 'Since everything runs on your machine, there's no upload delay...'
//                                                  ^ unescaped single quote
Enter fullscreen mode Exit fullscreen mode

This unescaped ' terminated the string early. The JS parser hit s no upload delay... and threw SyntaxError: Unexpected identifier. Every function defined after this line — including window.switchLang — was never created.

User-facing result: Clicking the EN/中文 language toggle produced window.switchLang is not a function. The entire bilingual system failed silently.

Fix: there'sthere\'s. Now I validate every i18n dictionary with node -e "$(cat file.html | sed -n '/RESIZER_I18N/,/^ };/p')" before deploying.

Bug 2: The Batch Script That Silently Missed Two Tips

I wrote Python scripts to auto-inject data-i18n attributes into HTML. The replacement logic:

('Use JPG at 80-85% quality for photos', ' data-i18n="tip2">Use JPG at 80-85% quality for photos')
Enter fullscreen mode Exit fullscreen mode

But the actual HTML contained — (HTML entity) in some places and (Unicode em-dash) in others. The string match failed silently. Two out of four Pro Tips never received data-i18n attributes.

User-facing result: In Chinese mode, Tips #2 and #4 remained in English while #1 and #3 translated correctly. Subtle. Embarrassing.

Rule I now enforce: Every batch migration script must include a verification pass:

expected_keys = ['tip1', 'tip2', 'tip3', 'tip4', 'why1Title', ...]
for key in expected_keys:
    if f'data-i18n="{key}"' not in html:
        print(f'MISSING: {key}')
Enter fullscreen mode Exit fullscreen mode

Bug 3: innerHTML vs. SVG Children

<!-- WRONG: applyI18n() calls innerHTML on this button, wiping the SVG -->
<button data-i18n="ariaMenu" aria-label="Menu">
  <svg>...</svg>
</button>

<!-- RIGHT: translate aria-label in swapLang(), keep SVG intact -->
<button id="hero-hamburger" aria-label="Menu">
  <svg>...</svg>
</button>
Enter fullscreen mode Exit fullscreen mode

applyI18n() assigns innerHTML for all non-input elements. If you put data-i18n on a parent containing an SVG icon, the icon disappears and gets replaced by raw text. The fix: keep data-i18n on sibling <span> elements, never on structural parents.

Bug 4: The Nginx Header That Broke Every Camera

The Microphone & Camera Test was failing on every device with Permissions policy violation. Users thought they had blocked access. They hadn't.

The Nginx server was sending: Permissions-Policy: camera=(), microphone=()

This header silently forbade all camera and microphone access before the browser even asked the user. The fix was a single Nginx config line change: camera=(self), microphone=(self). Users now see the normal browser permission prompt.

I also updated the deny-state copy in mic-camera-test.js (both English and Chinese) to explain that multi-device failures usually indicate a server header issue, not user settings.


Part VI: The Human Debug — When the Compiler Can't Help

On June 10, the developer's journal in our changelog reads:

"I woke up feeling off. Not the usual 'stayed up too late coding' off — something worse. Ended up at the hospital in the morning instead of at my desk. Lost half a day that I couldn't afford to lose. My body's been deteriorating for a while now. Too many late nights, too much coffee, not enough sleep, not enough care."

I shipped three tools that day. Only two made it. One of them was only 60% done.

On June 11, the entry isn't about code at all:

"When you pour real feeling into something and get indifference in return, the math never balances. You keep re-checking the equation hoping the answer changes. It doesn't. The hardest part isn't the leaving; it's that you genuinely cared and they genuinely didn't. But the only way past that truth is through it."

"To anyone who's ever felt like leaving this world because of one person, one heartbreak, one season of pain: don't. The world is wider than the wound. Green hills await wherever you roam."

The road trip was supposed to be June 12–15. Xinxiang, Xinyang, maybe Hunan. Two days of driving, windows down, nowhere to be. It got postponed. The keyboard won.

On June 8, I asked the AI Tarot: "Is there still a chance with her?" The three-card spread returned Judgement (Past) → The Devil (Present) → The Lovers (Future).

The AI read Judgement as a necessary awakening that had already happened — not a random ending, but a reckoning. The Devil nailed the present: caught in the grip of attachment, tethered by old patterns. And The Lovers: yes, there is a possibility — but only through authentic alignment, not compulsion.

It was unsettlingly accurate. The cards saw what I already knew but wouldn't admit.

Building in public means documenting the human bugs too. The insomnia. The weight loss (70.5 kg → 68.5 kg in two weeks). The appetite that vanished. The Douyin follow-then-unfollow at midnight. These are the entries that don't make it to the GitHub release notes but matter more than version bumps.


Part VII: The Numbers & What Honesty Looks Like

Metric Value
Total tools shipped 86 (85 in /tools/ + 1 root feature page)
Tools killed after shipping 2 (AI Pixel Art GIF ×2 attempts, Live Photo Frame)
Bilingual pages migrated 19
Legacy pages remaining 67
Blog posts published 91 (84 guides + 9 Tool Tales)
Changelog entries 46
Service Worker version toolknit-v119
AI token spend (peak day) $200
Hospital visits during build 2
Lines of code written I stopped counting after the insomnia started

What's Next (The Arithmetic of Patience)

The remaining 67 legacy pages will migrate gradually. Each one requires:

  • HTML structural overhaul (hero top-bar with language toggle, FAQ accordion, responsive footer-grid)
  • data-i18n / data-i18n-common attribute injection across all static text
  • Page-specific i18n dictionary creation (30–60 keys per page)
  • JSON-LD schema updates (SoftwareApplication + FAQPage + BreadcrumbList)
  • Manual testing: JS syntax validation, language toggle verification, mobile responsive check
  • Service Worker cache version bump

I won't rush it. The architecture is solid. The lesson from Live Photo Frame is clear: ship what you're proud of, even if that means shipping less.

The next batch will likely be the remaining PDF tools and video converters. Each shared library (convert-image.js, compress-image.js) reduces the per-page migration cost — the shared t() calls are already defined; only the HTML markup needs updating.


The Ask

If you've ever built something, shipped it, then had to kill it after two days of debugging — you're not alone.

If you've ever stared at an AI token bill and wondered if the architecture is worth the arithmetic — same.

If you've ever shipped code from a hospital waiting room, or debugged a production bug at 3 AM while your body begged for sleep — I see you.

ToolKnit is free. No signup. No uploads. All processing is client-side. Read the full changelog — it's more honest than most build logs.

Built with love. Paid in tokens. Still shipping.


Originally published as a raw build log from a solo indie project. No affiliate links. No sponsor reads. Just code, costs, hospital visits, and the occasional 3 AM SyntaxError. If you're building something alone right now — keep going. The grey skies pass. They always do.

Top comments (0)