Ninety-six commits, seven and a half weeks, one person. I built the whole thing, then audited it like someone else had.
Scroll: the ledger reconciles.
overview
Iran Broker New Tab is a Chrome extension that replaces the browser’s empty new-tab page with a working dashboard for Persian-speaking traders: four live markets, an economic calendar, a trading journal with real analytics, a focus timer, and an AI assistant called Aira.
I did all of it — the idea, the product decisions, the interface, the visual design, every line of the code, the QA, the documentation, and the Chrome Web Store package. Claude Code was the tool I built it with, the way a designer uses Figma; nobody else made a decision on this product.
Below is what I built, how I built it alone, the bug that nearly invalidated the whole journal, and the single problem that is still keeping it off the store.
Why I built it
I trade, and I kept six tabs open to do it: prices on one site, the economic calendar on another, a journal in a spreadsheet, a Pomodoro timer on my phone, the forum somewhere else again. None of them talked to each other, and every one of them cost a deliberate act of navigation.
Meanwhile the one page a browser guarantees you will see dozens of times a day — the new tab — was blank. That asymmetry is the whole idea. I was not trying to build a trading platform; I was trying to put the five things I already checked onto the surface I already opened.
The audience made it sharper. Persian-speaking traders get English-first tools with a right-to-left afterthought bolted on, and the data they actually care about — the Tehran Stock Exchange, the free-market dollar rate, the coin price — is exactly the data those tools do not carry. Nobody was going to build this for them, so the product had to be Persian-first in its layout, its calendar, its typography and its data sources, not translated into Persian at the end.
What I built
Everything below runs in a single page with no build step: plain HTML, CSS and JavaScript, nine independent scripts, no npm, no bundler, no ES modules. Nothing is a framework component; every widget owns its own `localStorage` key and fails on its own without taking the page down.
That constraint was a decision, not an accident. A new tab page has to paint before the user has finished pressing the keyboard shortcut. Anything that needs a bundle to boot has already lost.
Nine things that had to fit on one page
Every screenshot below is the extension running, captured from its own source.
01The page itself
A Jalali date, a greeting that uses your name and the time of day, the clock, and the one line that matters to a trader before anything else: which session is open right now, and in whose time. The search bar routes to Google or straight into Iran Broker’s own tools, and the dock in the corner opens the journal, the theme, settings and focus mode without leaving the page.
newtab.js · first contentful paint measured at 296ms, independent of every API
02Four markets, one column
Crypto, then forex with gold, oil and the dollar index, then Iran’s free market — dollar, gold, coin — and then the Tehran Stock Exchange. Four tabs over one panel, refreshed every ninety seconds, each row carrying its own sparkline. The Iranian data is the part no international tool ships, and it is the reason the product exists here rather than anywhere else.
coingecko · yahoo finance · call2/call3.tgju.org with failover · cdn.tsetmc.com
03Hub — the day, not the market
A second layout for the same page. The price columns give way to a Jalali calendar with the week’s economic events marked on it, an hourly weather widget, today’s task list, and a quick-access grid you fill with your own links. The clock, the search and the identity do not move — the page changes what it is about without changing where anything is.
js/hub.js · forex factory weekly feed · open-meteo · 1,527 lines
04The journal — logging a trade
Two modes: quick, for the fields you always fill, and full, for everything else. Symbol, direction, entry, exit, stop loss, take profit, lot size, commission — and the field most journals leave out, how you felt before you opened the position. Trades import from an MT4 history file and export as CSV that opens correctly in a Persian Excel, which sounds trivial until it is your data stuck in the wrong encoding.
js/journal.js · MT4 CSV import · localStorage key ib_journal_v1
05The journal — what the numbers say
Nothing here is typed twice. Win rate, profit factor, average R:R, mathematical expectancy, max drawdown, the longest streak, best and worst trade, and the equity curve are all derived from the trades you logged, filtered by any window from this week to all time. This is the screen that has to be arithmetically right, and the one place this project failed hardest before it passed.
derived stats · equity curve on canvas · R:R distribution histogram
06The journal — the part I have not seen elsewhere
Every trade carries the emotion you recorded before entering it, and this tab puts that against the outcome: calm, anxious, excited, confident, each with its own count, win rate and average P&L. Beside it, a calendar heatmap of the days you actually wrote something down. It is the cheapest feature in the product to build and the one most likely to change how somebody trades.
emotion tagging at entry · daily note heatmap · behaviour patterns
07Aira — answers in Persian, streamed
A chat assistant for the questions a new trader actually asks: which broker, what spread, what a prop firm is, how to size a position. The answer streams token by token so you can read it as it arrives, past conversations are listed down the side, and a lock button in the header turns on private mode — the message still goes to the model to get an answer, but nothing is written to local storage.
js/airo.js · streamed responses · private mode key ib_airo_no_history
08Focus mode
The whole tab becomes a timer: Pomodoro or a trading-session length, an SVG progress ring, ambient sound, session dots tracking the cycle, and keyboard control — space to pause, F for full screen, Escape to leave. It spends the extension’s only Chrome permission, notifications, on exactly one thing: telling you the session ended when the tab is not in front of you.
js/focus.js · web audio · the one permission in manifest.json
09Settings, and getting your data out
Four themes including a glass mode, four accent colours, and a background picker backed by live Pexels photography across six trading-related categories, cached for a day so a shared key is not burned in an afternoon. The data tab exports every `ib_` key to a JSON file on your machine and restores from one — no network, no server, no account. It rejects foreign keys in both directions and asks for confirmation inside the panel before it overwrites anything.
backup covers all 14 ib_* keys · no chrome.storage anywhere in the code
How I built it alone
Ninety-six commits between 27 June and 18 August 2026, and the shape of that history is the honest part. On day one I built a modular architecture with twenty-one feature modules. Seven weeks later I deleted all twenty-one, because I found they had never been loaded — the page ran on a completely different set of files the whole time. I also built and removed an embedded Telegram sidebar, a widget edit mode with drag-and-resize, and a forex news ribbon. Roughly a fifth of what I wrote never shipped.
Working with Claude Code made me fast, and fast is exactly how you end up with a twenty-one-module architecture nobody loads. So in the last stretch I changed what I was asking for. Instead of more features I asked for an audit, and I gave it one rule: the code is the source of truth, not the documentation, and not what I remembered deciding. Every document in the repo got rewritten from a direct reading of the live code, with that rule printed at the top.
The audit found the dead module tree, four domains in `host_permissions` with no caller left, a permission requested with zero call sites, and twelve of sixteen fetch calls with no timeout — able to hang in a loading state forever, which on an unstable Iranian connection is a normal afternoon. Then I ran seven QA tests against a real browser through Puppeteer, not by reading the source: network conditions emulated over CDP, all eleven storage keys deliberately corrupted, both market-data hosts blocked at the network layer. Four passed, two failed, one passed partially, and the tools are checked into the repo so the results can be reproduced rather than believed.
Seven and a half weeks, and what did not survive
96 commits, one author. Roughly a fifth of the work was later deleted — on purpose.- 27 JunInitial commit, then a three-column dashboard, a WebGL shader background and a modular architecture of 21 feature modules — all on day one.
- 28–29 JunFocus mode built, then rebuilt from scratch the same day. Settings modal, community sidebar, a Telegram sidebar added and reverted within hours. The journal arrives.
- 30 Jun – 12 JulHub mode, the first-run tour, the articles sidebar, the four price tabs, Aira. Widget edit mode is built — and then removed.
- 15 Jul – 11 AugPolish and performance: shader GPU cost, lazy hub data, Jalali calendar, redesigned weather. A forex news ribbon ships and is reverted.
- 15–18 AugThe audit. Dead module tree deleted, QA suite run, store package written, docs resynced against the code, P&L bug fixed, backup/restore added.
- 96commits
- 21modules built on day one, deleted in week seven
- 3features shipped and then removed
- 1author
The number that nearly invalidated the journal
One function assumed every symbol was a forex pair, so a $1,800 Bitcoin move became eighteen million “pips”. That single figure poisoned every statistic on the analytics tab — best trade, total P&L, profit factor, the equity curve. I found it while seeding fake trades for an unrelated right-to-left screenshot test, not while looking for it.
Seven tests, run in a real browser
Four passed, two failed, one partially. The failures are why it was worth running.- 1Visual regression at three widthspass
- 2RTL reviewfail
- 3API outage simulationpartial
- 4Timeout / AbortController auditfail
- 5localStorage integritypass
- 6Clean installpass
- 7First-paint timingpass
- 21dead files — the module tree I wrote on day one
- 4domains with no caller left in the code
- 1permission requested and never used
- 4.8 MBof bundled images, replaced by a live picker
Outcome
The extension is finished, documented, packaged for the Chrome Web Store, and deliberately unpublished. Everything a store reviewer asks for exists — privacy policy, permission justifications, data-disclosure form, MV3 compliance checklist, store copy, five screenshots — and it is still sitting there, because of one thing I have not fixed.
- 96 commits, 0 → 100idea, design, code, QA, docsevery decision and every line mine, built with Claude Code over seven and a half weeks
- 1 permission, 13 domainsthe access surfaceonly notifications, spent entirely on the focus-session alert; every other origin blocked by CSP
- not shippedthe decision I stand behinda hardcoded paid API key blocks release; publishing anyway would put an extractable key inside every install
Building alone with an AI that never gets tired removes the two things that used to slow me into good decisions: the cost of writing code, and somebody asking why. I shipped a twenty-one-module architecture on day one because it was cheap to write, and I did not notice for seven weeks that the browser never loaded it. Nothing forced me to look. What fixed it was not discipline, it was changing the instruction — asking for an audit against the code instead of asking for the next feature, and writing “the code is the source of truth” at the top of every document so that future me could not quietly believe the old story. I now schedule that audit rather than waiting for a suspicion, and I treat speed as the thing that needs a counterweight, not as the achievement.
Why it is not in the Chrome Web Store
Aira’s API key is hardcoded into code that runs in the user’s browser, where anyone can read it out of devtools — and it is a metered, paid key. Fixing it properly means putting a backend proxy in front of it, which is a different project from this one. So the package sits finished and unpublished. That is the honest state of it, and I would rather write that here than ship an extractable key inside every install.





