Selected work
What I’ve built
Founder · Solo
Dealer Recon Systems
A dealer management system I built on my own, and still run.
Reconditioning, inventory, sales and CRM, service, parts and accounting. Car, RV, marine, motorcycle and body shop operations. The web app is live. iOS and Android are with Apple and Google.
Live demo ↗dealerreconsystems.com ↗
- Flutter
- Dart
- Firebase
- Firestore
- Cloud Functions
- Node.js
- Twilio
The problem
Before a used vehicle can be sold it has to be inspected, repaired, detailed, photographed and priced. Every day it sits waiting costs the dealership money. Most software for this is built from a spec written by someone who has never stood in a recon shop, and it shows: the screens do not match the order the work happens in.
A pipeline with deadlines
Every vehicle moves through seven stages, each with a target in days. Overdue units surface on their own instead of someone walking the lot to find them, and a vehicle can't leave Photos until it has enough shots for a listing.
- 01 Intake 1 day
- 02 Inspection 2 days
- 03 Mechanical 5 days
- 04 Detail 2 days
- 05 Photos 1 day Needs 5+ photos
- 06 Pricing 1 day
- 07 Lot ready For sale
Multi-tenant by default
Each dealership's data is isolated by Firestore security rules that fail closed: a dealership without an active account can't read anything, and a record's dealership can never be changed once it's created. On top of that sit about 40 permissions grouped into roles, with per-employee overrides, two-factor login and lockout after failed attempts.
Clients
- iOS
- Android
- Web
One Flutter codebase, offline cache
Firebase
- AuthMFA, lockout
- Firestorerules scoped per dealership
- Storagevehicle photos
Cloud Functions · 37
- Scheduled backups
- Twilio SMScampaigns, inbound
- Export & CSV import
- Audit log & restore
Built for the lot
Technicians work where the signal is bad, so the app keeps a local cache and queues photo uploads until it's back online. VIN decoding, barcode and document scanning and e-signatures are built in, and an audit log can restore any earlier version of a record.
Everything else
There's no team behind it. Besides the app, backend and website, I handle networking, customer support, legal and payroll.
Software Engineer · Remote
Kik
Closing a fake-camera exploit without tipping off the people using it.
I worked on Kik's Android app, mostly on security, and designed the UI and UX for Kik X, a full redesign of the app meant to compete with Instagram and Snapchat.
- Java
- Smali
- XMPP
- XML
- HTML
- CSS
The exploit
Kik labeled photos taken with the in-app camera differently from ones picked from the gallery, which told the recipient a photo had just been taken. Modified builds of the app were sending gallery photos flagged as live camera shots, making it easy to catfish with someone else’s pictures.
Detecting a modified app
I wrote a function that generates a hash unique to each version of the app. The server stores the expected hash for every version and checks it at login, so a tampered build can’t pass itself off as a genuine one.
Failing quietly
Blocking modified apps would have told their authors exactly what tripped. Instead they could still log in, but every photo they sent was delivered as a gallery photo, real camera shots included, while their own app kept showing it as a camera photo. From their side the exploit still looked like it worked.
-
1
Modified app → Kik server
Logs in and sends its version hash
-
2
Kik server
Hash doesn't match this version. Login is allowed anyway.
tampered -
3
Modified app → Kik server
Sends a gallery photo, labelled as camera
source: camera -
4
Kik server → Recipient
Delivers it as a gallery photo
source: gallery -
5
Modified app
The sender's own chat still shows
source: camera
Personal project · Solo
Mogadishu
Recovering a 2003 game engine's rules from its machine code, then proving them against the running game.
Delta Force: Black Hawk Down ships as compiled x86 with no source, no symbols and no documentation. I rebuilt a working model of what its engine does: movement, collision, blast damage, mission scripting. Then I wrote an overlay that draws all of it on screen while you play, so you can see the collision hulls, the invisible trigger volumes, the grenade arcs and the kill radii the game itself never shows you.
- C
- Win32
- Direct3D 8
- Python
- Ghidra
- x86 assembly
What the overlay draws
The game will not tell you why you are stuck on nothing, where a mission is watching for you, or how far a grenade will actually reach. The overlay draws all of it in the running game. The collision shot below is the clearest single example: the artwork is an aeroplane, and what the engine thinks is there is a handful of coarse convex volumes that barely resemble it.
The binary cannot be disassembled
The Steam build is encrypted on disk. I measured it section by section: only 0.5% of the code section on disk matches what ends up in memory, at maximum entropy. Ghidra cannot open it at all. So I analyzed a different, unpacked build of the game statically, dumped the Steam version out of a running process, and rewrote its section headers so the dump could be read as a normal executable.
Carrying findings between builds
Addresses move between builds, and they do not all move by the same amount — I measured one function shifted by 0x960 and another a few hundred bytes later shifted by 0x9D0. Adding a fixed offset silently lands you in the wrong function. Instead I disassemble the bytes at a known address and wildcard only the fields that are allowed to move, then search the other build for that pattern. It either matches exactly once or it fails loudly. Because the wildcarded slots are read back after a match, a function's global variables come across with it.
Attaching without touching the game
The game imports d3d8.dll, and Windows looks in the application folder first, so a stand-in DLL sitting next to the executable gets loaded by the game itself. No injection, no debugger, no patched files. That is partly principle and partly necessity: the Steam wrapper checks the executable for changes and refuses to start if a single byte differs.
Reloading code the game is holding open
A mission takes over thirty seconds to load, which makes restart-per-change unworkable. The proxy DLL holds only the permanent hooks and forwards through a pointer that is allowed to be null; the overlay itself is a second DLL, loaded from a shadow copy so a rebuild can overwrite the original while the game is running. The new build is live in about a second. Keeping the hooks in the part that never unloads is what stops the swap leaving the graphics driver pointing at freed memory.
Reading the game's own archives
Most of what the tool knows offline comes out of the game's packed archives, whose formats are undocumented. I decoded the archive container, the terrain heightmaps, the AI navigation meshes and the tactical map assets. Some of it corrected earlier work of my own: the navmeshes were being classified by file extension, and going by the actual file signature instead turned up dozens more hiding under the wrong names, including two calling themselves images.
Reading compiled mission scripts
Missions ship as compiled bytecode with no opcode names anywhere in the game — it switches on bare integers. The shipped mission editor has to turn those same numbers back into readable text for its own UI, so its jump tables are effectively a translation key, and scraping them recovered the vocabulary. The overlay now renders a mission's live logic as English: which zone is being watched, how big it is, how often the script bothers to look, and what happens when the rule finally fires.
Labeling what I actually know
Every claim in the analysis notes is marked as read from the disassembly with an address, derived by arithmetic with the working shown, or a guess. It currently stands at 444 read, 150 derived and 40 guesses. Thirty-one documents carry corrections: a function two of them called the damage handler turned out to pick the death animation, and an earlier claim that low frame rates made the player faster was tested from 10 to 106 fps, found false and withdrawn rather than quietly deleted.
Two routes to the same number
Reading the code and measuring the running game are checked against each other. Walking speeds pulled from animation data agree within about 1% with speeds measured by driving the game through posted keystrokes. A grenade at the player’s feet was predicted to throw them at 7.69 m/s and measured at 7.7, with the damage exactly as calculated. Where there is no number to find, nothing is drawn — the AI’s line-of-sight check turns out not to consider smoke at all, so the overlay shows no smoke radius.
What it deliberately will not do
The tool is single-player analysis and is built to stay that way. It never touches networking, matchmaking or anti-cheat code, and every drawing is gated on a single-player check read out of the game’s own mission loader that fails closed — if the flag cannot be read, the overlay stays off. The source is MIT licensed and ships no game content; you supply your own copy.
Back lot
Tools and parsers
-
Hindsight
Chess game review that runs on your own computer, free — the thing Chess.com charges for. Load a PGN and Stockfish grades every move by how much it gave away, scores accuracy on the same curves Lichess uses, and explains each mistake by finding the actual pin, fork or hanging piece on the board. Version 0.1.0 is out with installers for Windows, macOS and Linux, and CI runs all 581 tests on all three. No account, and nothing leaves the machine.
Download and source on GitHub ↗
- TypeScript
- Electron
- Stockfish
-
paeth
A PNG decoder written from the spec in Rust. Chunks, inflate, the five filter types, Adam7 interlacing. PngSuite is 176 images built specifically to break decoders — paeth decodes all 162 valid ones pixel-for-pixel identically to an independent decoder, and rejects all 14 corrupt ones for the reason the suite documents. 151 tests, and a fuzz pass that mutates headers to check nothing panics.
- Rust
- PNG
- zlib
-
Loupe
Prints what is inside an executable: headers, mitigations, sections, imports, exports, for both PE and ELF. Read-only by design — it never patches, writes or runs anything. Every offset in a binary is attacker-controlled, so every read goes through a single bounds-checked accessor. It parses all 4,264 binaries in System32 under AddressSanitizer, UBSan and LeakSanitizer with no crashes, no memory errors and no leaks.
- C
- PE
- ELF
-
unspool
Reads pcapng capture files and decodes what is in them: Ethernet, IP, TCP and UDP, DNS, TLS handshakes, HTTP. Parsing only — no capture, no driver, no admin rights, so it installs anywhere. Checked frame by frame against tshark across all 562 public Wireshark sample captures, 1,290,282 packets. 172 tests.
- Python
- pcapng
- DNS/TLS/HTTP
-
vinspect
Validates and decodes Vehicle Identification Numbers. The check digit is pure arithmetic, so it catches most typos before anything hits a database. Offline decode gives manufacturer, model year and plant from a compiled-in NHTSA registry snapshot, and where the model year is genuinely ambiguous it returns both candidates instead of guessing. Verified against 6,516 real VINs from NHTSA's crash-test records. 108 tests, one static binary, no network needed.
- Go
- ISO 3779
- NHTSA vPIC
-
Unity runtime injection
An injector that loads C# into a running Unity game and hot-swaps it without a restart, using a small hand-written x64 stub that calls into the Mono runtime. No mod frameworks.
- C#
- x64 assembly
- Mono
Mathematics
Some math I animated
Each one runs off the real math, so the picture and the claim can't drift apart. Every card sits in exactly one of six branches, so picking a branch actually changes what you're looking at.
A separate script re-derives every number these animations put on screen. It shares no code with them, so when the two agree it's because two independent routes landed in the same place. It has caught six real mistakes of mine: a claim that was flatly false, an integral off by a factor of two, a physical constant off by the same, a rigged comparison, something I called finite that isn't, and an infinite loop that took the whole page down. The sunflower, the quickest way down, and the area and the slope each own up to one.
-
Geometry
Shear, turn, shear
Euclid's proof, performed as the three moves it actually is. Lay the hypotenuse flat and put its square underneath; the altitude dropped from the right angle cuts that square into two rectangles, one owed to each leg. Now take the square on a leg and shear it — slide its far edge along its own line, which leaves the base alone and the height alone and therefore leaves the area alone — then give it a quarter turn about the corner, then shear it once more. It lands exactly on its rectangle, and nothing was ever stretched. The two areas in the readout are shoelaced off the moving shapes on every frame, so a wrong move would show up as a number drifting.
-
Geometry
Two thirds of a cylinder
Archimedes wanted this on his gravestone and got it. Slice a hemisphere and a cone at the same height and the discs you expose have areas π(r²−h²) and πh², which add to πr² — the cylinder's disc, at every height without exception. So the cone is a third, the hemisphere is the rest, and a sphere is two thirds of the cylinder it fits inside. Note the animation shows discs and not widths: it is the areas that add, not the radii.
-
Geometry
The volume of a donut
Sweep a shape around an axis it never crosses and the solid's volume is the shape's area times the distance its center travels. Rather than assert that, the animation cuts the finished donut and straightens it: the inside of the bend compresses exactly as much as the outside stretches, so what you are left with is an ordinary cylinder of radius r and length 2πR, and nobody needs convincing about the volume of a cylinder.
-
Geometry
Why π is there at all
Roll a wheel through one turn and it lays down its own circumference. Measure that track in diameters and three fit, with a bit left over. What the wheel can show you is that the leftover is the same for every wheel, whatever size you build it, which is the reason the ratio is worth naming. What it cannot show you is that no fraction ever lands on it exactly; that stayed an open question until Lambert proved it in 1761. The marked point on the rim draws a cycloid on the way, a better curve than it has any right to be: one arch is exactly 8r long and encloses exactly three times the area of the wheel that drew it.
-
Trigonometry
Where the wave comes from
Sine and cosine are not formulas, they are coordinates. Walk a point round a circle of radius 1: its height above the axis is the sine of the angle and its distance along is the cosine, and carrying that height out to the right as the point moves draws the wave on its own. Watch the heavy arc in particular — its length is the angle. That is the entire content of the word radian, and it is why the derivative of sine is cosine with nothing stuck in front of it; measure in degrees and you are left dragging a factor of π/180 around forever. The triangle inside the circle has legs cos and sin on a hypotenuse of 1, so sin² + cos² = 1 is Pythagoras and nothing more, and the readout holds it at 1.000000 while everything else moves.
-
Trigonometry
Corners out of circles
A square wave is only odd harmonics stacked up: (4/π)·Σ sin((2k−1)t)/(2k−1). Bolt each term to the end of the last as a rotating arm and the chain draws the wave by itself, flat tops and sharp edges out of nothing but circles. It never quite settles: the overshoot stays at about 9% of the jump however many arms you add. Watch what that means on screen — the wave runs between −1 and 1, a jump of 2, so the partial sums keep peaking near 1.179 instead of flattening at 1. That is Gibbs' phenomenon, not a rendering fault.
-
Calculus
The area and the slope
The fundamental theorem of calculus is one sentence: the rate at which the area is growing is the height of the curve. Above, the signed area under f fills in behind a sweep line — orange where the curve is over the axis, blue where it is under it and the area is being handed back. Below, that running total is drawn as a curve in its own right, and it returns to zero at 2π because the two humps are mirror images and cancel exactly. The two numbers in the readout are got from opposite ends on purpose. The area is accumulated by trapezoid off the top curve; the slope is then measured off the bottom curve by finite difference. Neither one is f evaluated and relabeled, which would be assuming the very thing on display. They agree to four decimal places anyway, and the sliver left over is not sloppiness: central-differencing a trapezoid sum collapses to (f₋₁ + 2f₀ + f₁)/4, a smoothing average rather than a difference quotient, which sits off f by h²f″/4. The checker works that number out from the step size before it measures it and gets the 1.4×10⁻⁵ the readout drifts up to at worst. I first wrote six decimal places here, then five. The measured gap says four, so it has caught me on this one sentence twice.
-
Calculus
Finite to fill, endless to paint
Spin y = 1/x about the axis, starting at x = 1, and run it out forever. The volume converges on exactly π and stops. The surface area grows like 2π·ln x — a crawl, but a crawl with no end — so here is a shape you can fill with a finite amount of paint and never finish painting. The paradox only bites if you imagine paint as a coat of fixed thickness, which nothing coating a shape that narrows to nothing could be. Filling it coats it.
-
Number theory
Cross out, never divide
Eratosthenes, and still the way you would actually do it. Write the numbers out, take the first one nobody has crossed off, strike out its multiples, repeat; whatever is left standing is prime, and not one division was performed. Two details are load-bearing rather than decorative — striking starts at p² because every smaller multiple of p carries a factor below p and was caught by that one already, and the whole procedure can stop the moment p² runs past the end. What comes out is exact: 62 primes below 300. Beside it sits N/ln N, the leading term of the prime number theorem, guessing 52.6. That is fifteen per cent low, and because the relative error only fades like 1/ln N it is still 7.8% low at a million.
-
Number theory
It never closes
z(θ) = e^iθ + e^iπθ. Two arms: the second bolted to the tip of the first, spinning exactly π times as fast. The pen draws a rose whose petals all touch a circle of radius 2. For the curve to close, some whole number of laps would have to make π whole too — so it never does. Lap 7 comes back within 0.0556 (that's π ≈ 22/7) and lap 113 within 0.00019 (π ≈ 355/113); the animation dives into the lap-7 near miss so you can watch the new line run alongside the old one and miss it. After two million laps the closest it has ever come is 2.4×10⁻⁷.
-
Number theory
The angle no fraction gets near
The same fact as the card above, with the worst-approximable number standing in for π. Plant each seed a fixed angle round from the last. Pick a simple fraction of a turn and every seed lands on one of a few spokes forever — three eighths gives eight spokes and no number of seeds will fill the gaps. The golden angle is the hardest of all angles to mistake for a fraction, so no spoke family ever closes. What this is not is a packing record: at 500 seeds, 137.7° leaves a smaller hole than the golden angle itself does. I had written the packing claim, and the checker caught it.
-
Probability
π by throwing darts
The quarter circle covers exactly π/4 of the square around it, so scatter points at random and the fraction landing inside converges on π/4. No formula, no calculus — just counting, and the patience to count enough. The generator is a plain linear congruential one, which is normally a mistake here, because pairing successive draws puts them on a lattice. At 2400 darts it does not bite; the checker runs 600 seeds to confirm that.
-
Probability
A bell curve, assembled by falling
Twelve rows of pegs, a fair coin at every one, and a ball's bin is nothing more than how many times it went right. So the bins fill in proportion to C(12,k)/4096 — a binomial distribution built out of gravity and a fair choice, with no statistics done to it. The stepped outline the bars climb into is that exact binomial, not a curve fitted to the bars after the fact. The dashed line through it is the normal approximation, and the two are drawn apart on purpose: at the middle bin the binomial says 0.225586 and the normal says 0.230330, so the bell curve everyone reaches for is 2.1% high in the one spot where it looks most convincing. More rows narrow the gap. Nothing closes it.
-
Physics
The quickest way down
Three beads, three tracks, same start and same finish, released together. The cycloid wins — the curve a rolling wheel's rim traces out, the one the geometry group leaves you with — because it trades length for an early steep drop and spends the rest of the trip fast. The straight ramp is the shortest route and takes 18.5% longer; the circular arc is a fair opponent and takes 2.5% longer, both measured on the cycloid's clock. Then the stranger half: put beads at different heights on one arch and they all reach the bottom together, in π√(a/g) whatever height they started from. Huygens built a clock on that. One opponent had to be withdrawn entirely. I used to say an arc whose tangent starts horizontal lost by a factor of ten; it loses by nothing finite. Start a bead at rest on a level tangent and the drop grows as the square of the distance traveled, so the descent integral diverges and the bead never arrives at all. Every time I ever quoted for it was a statement about my own step count and nothing else.
-
Physics
What a pulley actually buys you
Count the rope segments holding the moving block up — that number is the whole trick. Four segments share a 600 N load, so you pull 150 N. But those four segments each have to shorten by however far you want the load to rise, so you haul four metres of rope for one metre of lift. Force divided by four, distance multiplied by four, and the product is identical in every configuration: 600 joules in, 600 joules out. A pulley buys you a smaller force and charges you the exact same work.
-
Physics
Equal areas, equal times
Kepler's second law. A planet on an ellipse sweeps area at a constant rate, so it tears around the near side of its orbit and crawls along the far one. The eight wedges below cover eight equal slices of time and look nothing alike: one short and fat, another long and thin. The readout shoelaces each wedge off the drawing as it completes and reports the range across all eight, so what you see is the real spread between them — about 9×10⁻⁵. Read that as the drawing's fault rather than Kepler's: each wedge is closed with 48 straight chords, and a chord cuts the corner off a curve. The law itself is exact. What the readout used to do was compute πab/8 once and print that same constant eight times, which proved nothing at all. Underneath it is conservation of angular momentum: r²θ̇ is fixed, and the area swept per second is exactly half of it.
Chess hall
A game I won
Rapid, playing Black. White opened quietly and then spent six moves walking the queen around the board; once the position opened I traded into an attack and finished it on the g-file. The board below plays the whole thing through — every move is read from the PGN by a move generator I wrote, not a hardcoded list of squares.
Chess.com · rapid · 29 July 2026 · 0–1
White Guest5184350737 Black Guest6258940814 — me
- 1.e3e5
- 2.c3d5
- 3.Qa4+Bd7
- 4.Qb3Bc8
- 5.Nf3Nc6
- 6.Qa4e4
- 7.Nd4Bd7
- 8.Nxc6Bxc6
- 9.Bb5Ne7
- 10.Bxc6+Nxc6
- 11.b4a6
- 12.b5axb5
- 13.Qxb5Qc8
- 14.a4Ra5
- 15.Qb3Bd6
- 16.Ba3Bxa3
- 17.Rxa3O-O
- 18.O-ONe5
- 19.Qb4Nf3+
- 20.gxf3Ra6
- 21.fxe4Qg4+
- 22.Kh1Qf3+
- 23.Kg1Rg6#
The finish is worth a look. 19...Nf3+ is offered rather than played — taking it with 20. gxf3 is close to forced, and it strips the pawn off g2 and opens the file the rook eventually mates on. By 23...Rg6# the king has nowhere: f1, f2 and h2 are all blocked by White's own pieces, and g2 and h1 are covered by the queen on f3.
About
Background
I worked in dealerships for years before I wrote any software for one. That's the whole reason DRS is shaped the way it is: it follows how a car, a tech and a manager actually move through a day, because I spent a long time watching them do it.
Outside work I pull software apart to see how it was put together. File formats, bytecode, network traffic, code loaded into processes that are already running. Same habit that turned up the Kik bug.
Front desk
Hiring for a fullstack, mobile or security role?
I'm looking for a remote team to join. Email's the fastest way to reach me and I answer everything.