Steam trading-bot service
A separate service that sends trade offers to customers with steam-tradeoffer-manager and follows each one through to an accepted trade.
Software developer · Denmark
I build products end to end — storefronts, trading bots, data platforms, music apps. Below: every commit I've made, on two GitHub accounts, as a city.
2017 → today
contributions · 0 active days · 2 accounts · 1 developer
github.com/piijt + github.com/peterhjespersen
Side projects on my personal account, product work on my work account. Merged day by day, every year since 2017.
live Case study · 2019 → today · solo-built
Trade your CS2, DOTA2, RUST and TF2 skins for World of Warcraft gold. I designed, built and run the whole platform — storefront, API, Steam trading bots and pricing engine.



The core loop
Under the hood
A separate service that sends trade offers to customers with steam-tradeoffer-manager and follows each one through to an accepted trade.
Converts Steam market prices into gold per game version and region, layered with event bonus percentages and a +10% bucket bonus for selecting more than 10 items.
Rust-style stacked items are handled as quantities, so a stack is priced, selected and traded correctly instead of as a single skin.
Side project · music
Your music, saved and always playing.
A self-hosted music library: search YouTube or import a Spotify playlist, build playlists, and play it all from your phone with the screen off. That last part drives the entire architecture.
Friday 9 October
9:41
Live recreation · tap anything
A tiny synth built live with WebAudio, kept quiet. While it plays, the visualizer reads a real AnalyserNode instead of the synthetic beat.
Architecture
Every box is a decision the lock screen forced. Tap one for the detail that makes it work.
YouTube
yt-dlp has no continuation cursor, so a deeper page means asking for a larger N. Each query is cached for 10 minutes, over-read by 40, and merged rather than replaced, so pages never overlap when YouTube reshuffles.
ytsearchN: · LOOKAHEAD 40 · merge, don't replaceSpotify import
Each Spotify track is searched on YouTube and every candidate is scored, with duration weighted most because a different length almost always means a different recording. Below 0.40 is reported as not found; below 0.72 is flagged for you to check. This runs the real formula.
3 matched · 1 to check · 1 not found → a matching flow playlist, name and cover included
Every search result plays as a preview within seconds. Add promotes it in place, no second download; anything you don't keep is swept after a few hours.
Opt-in video per track. The <audio> stays the clock: big gaps snap into place, small drift is absorbed by nudging playbackRate a few percent. Toggling never interrupts playback.
Follow people, share your library per follower, follow their playlists, see friend activity and join them at their position. Messaging needs a follow both ways.
argon2 hashes in MongoDB. The admin is seeded on startup; everyone else joins through an expiring invite link generated on the Admin page.
Docker Compose for the app and MongoDB, Caddy for automatic HTTPS. The SPA is prebuilt and served by FastAPI, so there is no Node runtime in production.
Add to home screen over HTTPS and it behaves like a native player. The service worker caches only the shell and hashed assets, never /api/.
Product · data · SaaS
A multi-tenant data workspace I designed and built end to end. Point it at MySQL, Postgres or MongoDB, save a query, and every run is captured as an immutable, versioned snapshot: diffable, schedulable and ready to chart. Every row, query and API key is fenced off by organization.
SELECT region, COUNT(*) AS orders, SUM(total) AS revenue, status FROM orders GROUP BY region, status; read-only ✓Click a source to push a burst of rows · click a card to open that version
| region | orders | revenue | status | |
|---|---|---|---|---|
| EU-West | 289 | 10.7k | paid | |
| APAC | 277 | 12.3k | paid | |
| Nordics | 133 | 5.4k | paid | |
| ~ | US-West | refunded | ||
| LATAM | 208 | 9.2k | paid | |
| MEA | 84 | 3.2k | paid |
schema region:string · orders:int · revenue:float · status:enum
// decoded JWT { "sub": "usr_4c1e", "role": "editor", "orgId": "org_acme" }
Click an organization to sign in as it
What's next
© 2026 Peter Jespersen