Pahlavon Numonjonov Case study
exchange
← All work

exchange

Central-bank and exchange-office rates with history, charts and threshold alerts — written against the Python standard library and nothing else.

Role
Solo — design, build, run
Timeline
2026 — present
Stack
Python 3.11 · stdlib only · SQLite
Status
Running

The problem

Exchange rates in Uzbekistan come in two flavours: the official central-bank rate, and what you are actually offered at a bank window — and the gap between them is the whole point. Nobody publishes both in one place with history, and the apps that come close want an account, show ads, and stop working when their API key expires.

I wanted a rate tracker I could still run in five years without touching it.

The constraint that shaped everything

Zero dependencies. requirements.txt is empty on purpose. HTTP is urllib, storage is sqlite3, the Telegram client is written against the raw Bot API, and charts are drawn by emitting SVG and rasterising it by hand. Nothing outside the Python standard library.

This started as an exercise and turned into the most defensible decision in the project. A dependency is a promise someone else has to keep. The failure mode I was designing against was not a bug — it was coming back after a year to a project that will not install.

What it cost

  • Charts. No matplotlib means writing the axis scaling, tick selection and label placement myself, then a minimal PNG encoder to hand Telegram an image it accepts. That is a weekend I would not have spent otherwise.
  • The bot. No aiogram means implementing longpolling, offsets, retries and update routing by hand. Roughly 300 lines that a library gives you free.
  • Async. Without a framework, concurrency is not free either, so collection is sequential. For a job that runs a few times a day against a handful of sources, that is the right amount of engineering.

What it bought

  • git clone && python3 run.py works. No virtualenv, no install step, no lockfile drift, nothing to audit.
  • Every line in the repository is a line I can explain. When a rate looks wrong, the distance from symptom to cause is short.
  • The Telegram client and the chart renderer are useful on their own, because they carry no framework with them.

Architecture

cbu.uz banks_uz … one file each collect.py one source fails, the rest still run SQLite one file, full history CLI Telegram bot charts + alerts SVG → PNG by hand
Sources are interchangeable files behind one interface; everything downstream reads SQLite.

Screens and states

$ python3 run.py rates USD 12 680.00 CBU +12.00 USD 12 810.00 bank A +15.00 EUR 13 940.00 CBU -4.00

CLI. The same core the bot uses. If it works here, it works there.

/chart usd 30 [ PNG: 30-day line, drawn without matplotlib ]

Charts. Axis scaling, ticks and the PNG encoder are all in-repo.

/alert usd above 12800 ✓ saved — I will message you when it crosses.

Alerts. Thresholds in SQLite, checked after each collection pass.

collect: banks_uz FAILED (layout changed) collect: cbu.uz ok collect: 4/5 sources stored

Partial failure. A broken scraper is normal operation, not an outage.

Results and lessons

0
third-party packages, by design
0
breakages caused by an upstream release
1 file
is the entire database — backup is cp

What I would tell myself at the start

  • Isolate the part that will break. Bank websites change markup; that is not a possibility, it is a schedule. Each source is one file behind one interface, so a break is contained and a fix is obvious.
  • Write the constraint down in the repository. The rule lives in CLAUDE.md, because six months later a helpful person — often me — reaches for requests without knowing the empty file was a decision.
  • “No dependencies” is not the same as “no abstractions”. The standard library gives you less, so the seams have to be drawn deliberately.