StockIntel
C# / .NET 8 · Python 3.13 · PostgreSQL · RabbitMQ · Docker · Testcontainers
Whenever an executive or board member buys or sells shares in their own company, they have to tell the SEC within two business days, on a filing called a Form 4. All of it is public. StockIntel pulls those filings in and watches for patterns that are usually worth a second look, like three different insiders at the same company buying within a couple of weeks of each other. It's a learning project, and I set one rule for myself at the start: nothing gets added until there's a real problem for it to solve. That's why you won't find Kafka or Kubernetes in here.
It's made of two services that talk through RabbitMQ. A .NET 8 API handles pulling filings in, saving them and serving them, and it's the only thing allowed to write to the database. A Python service listens for new filings, keeps a rolling 14-day window for each company in memory, and runs three checks on it: several insiders buying together, an officer buying, and someone's stake jumping by more than a quarter. When one of those trips, it sends a signal back, and the API stores it and serves it up through a paginated endpoint.
The interesting problem is keeping a filing and the message about it from ever getting out of sync. If I save a filing and the broker happens to be down when I publish, nobody downstream ever hears about it. If I publish first and then the save fails, I've announced something that never happened. There's no single transaction that covers both. So the message goes into an "outbox" table inside the same database transaction as the filing, and a small relay empties that table into RabbitMQ every two seconds, fifty at a time. If RabbitMQ goes down, messages just pile up in Postgres and go out once it's back. The system gets slower for a bit, and nothing gets lost.
The catch is that a message can now show up more than once, so everything downstream has to shrug off duplicates. Filings are keyed on their SEC accession number with a unique index. Instead of checking first and inserting second (which can race), the code just tries the insert and handles the conflict. My favourite detail in the whole repo came out of this. When a duplicate gets caught, I clear out every pending change Entity Framework is holding, not only the filing. When I wrote it, that was just tidy-up. Once the outbox existed, it became the only thing stopping a duplicate filing from sending out a ghost message, and now there's a test that pins it in place.
I wired the RabbitMQ side by hand, no MassTransit, and that was on purpose, because I wanted to understand retries and dead-letter queues before letting a library hide them from me. Consumers only acknowledge a message once they're done with it. A message that fails waits ten seconds in a retry queue and gets three tries, and anything that can't be processed ends up in a dead-letter queue with a note explaining why. One thing you only find out by building it: when RabbitMQ dead-letters a message, it rewrites the routing key, so I have to dig the original back out of the x-death header. And on the way in, SEC lets you hit their servers ten times a second. I stay at five, because getting rate-limited by a government API is a bad way to spend a weekend.
Two languages means two chances to disagree about what a message looks like. Whichever side publishes an event also writes out sample files for it, and both test suites check against those samples, so a change that would break the other service fails in CI before it's merged. There are 130 tests right now, 107 in xUnit and 23 in pytest. The ones that touch Postgres or RabbitMQ run against the real thing in throwaway Docker containers, since a mocked database can't tell you whether your unique index actually fires.
What I'm working on now is a second listener on the same exchange that writes each user a short, plain-English recap of insider activity on their watchlist. An LLM writes it, its output gets checked against a strict schema, it runs on a token budget, and results are cached in Redis so the same summary never gets paid for twice. After that comes putting the whole thing online. It only runs as a single instance today, and I know the three reasons why: the relay would double-send if two copies ran at once, database migrations run at startup, and a second copy would double the traffic to the SEC. Each has a known fix, and that list is next.
StockIntel on GitHub
Wardrobe recommendation app
React · Node.js · Express · Python · scikit-learn · Firebase
This was a four-person team project that tries to answer the question everyone asks at seven in the morning: what do I wear today? You add what's in your closet, the app checks the weather where you are, and it suggests a few outfits. You tell it which ones you actually liked, and over time it gets a little better at guessing.
I co-led the team and owned how everything fit together, meaning the overall architecture, the API design, and how the four of us used Git without stepping on each other. That last part took more effort than I expected. We landed on short-lived branches and pull requests that at least one other person had to read before anything got merged, which saved us more than once the week before the demo.
The backend is Node and Express sitting on Firestore. It keeps track of your wardrobe as you add, edit and remove pieces, takes the messy response from a weather API and boils it down to something simpler like "cold, windy, might rain," and records how you rated every suggestion so the model has something to learn from.
Recommendations happen in two passes. The first is plain rules: no shorts at minus five, no suede in the rain, nothing too casual for a formal event. Whatever survives gets scored by a scikit-learn model trained on the outfits you've said yes to before, and the top few become your options. The model is Python and the server is Node, so the server launches the scoring script as a child process, hands it the candidates and reads the scores back. It isn't a fancy bridge, but it let each half stay in the language it's best at, and we could swap the model without touching the API at all.
Wardrobe app on GitHub
This desktop (the site you're on)
HTML · CSS · JavaScript · AWS S3 and CloudFront
I wanted a portfolio people would actually remember, and something about Windows XP makes just about everyone smile a little, so here we are. Everything you're clicking on is plain HTML, CSS and JavaScript.
The windows behave like the real ones. You can drag them by the title bar, resize them from any edge, minimize them to the taskbar, and maximize them and have them come back to exactly where they were. Whichever window you clicked last comes to the front. Each one also has its own link, so adding #win-projects to the address opens straight to this one.
You don't need a mouse for any of it. Alt+Space opens the little menu in the corner of a window, same as XP, and from there the arrow keys move or resize it and Enter locks it in. Tab and the arrow keys get you around the icons and the taskbar. The full list is in Help and Support.
Underneath the desktop, it's just a normal page. With JavaScript turned off, or if you print it with Ctrl+P, you get a clean, readable version of everything here. The icons and window borders all come from a single SVG sprite and a set of CSS variables, which is also how the three colour themes swap. It's hosted on S3 behind CloudFront, and the account that deploys it can't touch anything except this one bucket.
This site on GitHub