Adam Dent
I'm Adam, a QA Engineer with a curiosity for building software and solving problems.
I enjoy creating tools, experimenting with new technologies, and turning ideas into working projects. This site brings together some of the things I've been building, along with ways to get in touch.
Whereabouts live
Open Whereabouts How it works Source code
Overview
Whereabouts helps people find houses in North Yorkshire that mainstream mapping apps cannot. Thousands of rural properties are known by a name rather than a number ("Rose Cottage", "The Old Vicarage"), and most of those names mean nothing to Google or Apple Maps, leaving anyone trying to reach one guessing: delivery drivers, paramedics, district nurses, visiting friends. Type the house name, see the exact house ringed on a hand-drawn village map, and get turn-by-turn directions. It works fully offline once loaded, because the places it's needed most are the places without signal.
Built on Dr A Colin Day's free village maps (colinday.co.uk): 865 maps covering around 42,000 named houses, nearly 7,000 of them pinned by hand so far, district by district. More counties planned.
- Pipeline
- Python 3.12: pdfplumber, PyMuPDF, FastAPI
- App
- vanilla JS PWA, Fuse.js search, service worker
- Dataset
- 865 maps, ~42,000 houses, ~660 KB gzipped
- Hosting
- Cloudflare Pages
- Licence
- MIT (code), CC BY-SA 4.0 (location data)
A found house: ringed on its village drawing, one tap from directions.
How it's built
A Python pipeline (pdfplumber, PyMuPDF) downloads each village PDF and separates three kinds of integer text printed on it: the numbered legend, an alphabetical cross-reference with dot leaders (the source of house-name aliases), and the number labels scattered across the drawing itself. The legend is identified by the column geometry of its right-aligned numbers rather than by text matching, which survives typesetting quirks a line-by-line reader trips over. Each sheet is rendered to an image at 200 DPI.
Coordinates are captured with a purpose-built placement tool: a FastAPI server driving a Leaflet map, with the drawing overlaid semi-transparent on satellite imagery through a CSS matrix3d transform. The overlay's corners stretch independently to fit the drawing to the real streets; each house is then clicked once, recording latitude and longitude plus the pixel position used to draw the highlight ring. Saves are atomic and committed to version control automatically.
Houses are pinned one at a time in the placement tool.
The app is a static, framework-free PWA. Fuse.js fuzzy search runs over the full 42,000-name index (about 660 KB gzipped), tolerating typos and partial matches, and indexing the aliases some houses carry in the maps' cross-references. A service worker caches the shell and any viewed map, and whole districts can be saved offline as content-hashed WebP images (around 10 MB per district), so a revised map is fetched automatically and its stale cache entry pruned. Navigation hands off to Apple or Google Maps with a platform-detected deep link.
Trust details are explicit in the UI: a hand-placed house gets a solid pulsing ring; one positioned only from the drawing's printed label gets an amber dashed ring; and directions for a house not yet placed fall back to the village centre, with the interface saying so rather than pretending.
Challenges
The original plan was automatic georeferencing: place control points on each drawing and solve for a transform that derives every coordinate. The drawings are hand-drawn village plans rather than surveys, and the residual error was too large to pick the right house in a terrace, so the pipeline pivoted to direct placement. It is far slower: full coverage means months of manual pinning. But it is the only way to do it properly, and every pin is exact.
Parsing the PDFs produced a stream of edge cases. One village printed its legend numbers 1.7 points from the house names, below the tokeniser's word-merge threshold, so the parser detects that failure signature and retries at a tighter tolerance. Another sheet is a raster scan with no extractable text at all, transcribed into a fixture file, all 56 houses. And village names repeat across North Yorkshire while sheet IDs double as placement filenames, so a second Newby silently overwrote the first until discovery learned to namespace collisions by district.
283 groups of houses produce identical search results (four Rose Cottages in Dalton alone), so affected results carry a warning computed from the dataset rather than a guess.
Clamber closed beta
Overview
A social app for documenting and appraising path furniture: stiles, gates, footbridges, signposts, the built objects of the countryside footpath network. A member photographs one, appraises it on four dimensions (sturdiness, character, crossability, setting), and the record is shared with their friends.
The conceit is a venerable society of country gentlefolk, and every line of system copy holds that register. React Native (Expo) with TypeScript on the client; Postgres with PostGIS behind it. One codebase targets both iPhone and Android; currently in closed beta on Android.
Logging is a field-journal entry: photograph, location, the kind of object (chosen from a taxonomy of stiles, gates, crossings and collectables), the four appraisal sliders (haptic ticks on every step, dragging on the UI thread at 60fps), optional hazard notes, and field notes.
- Client
- React Native (Expo), TypeScript
- Backend
- Supabase: Postgres + PostGIS, Auth, Storage, Realtime
- Maps
- Mapbox, custom-styled Atlas
- Offline
- SQLite capture queue
- Testing
- Jest + RN Testing Library, Maestro end-to-end
- Status
- closed beta, Android
The Feed: each card is a Piece with its aggregate appraisal; outstanding ones carry their Grade.
The Society
The data model separates the object from the observation. A Piece is a specific real-world object with a location and a type; a Sighting is one member's photograph and appraisal of it. A new post is proximity-matched to an existing nearby Piece of the same type or creates one, and the first member to document a Piece is credited permanently: discovery prestige in place of follower counts.
Members bestow gestures on a Piece (a doff of the cap, a tut, a marvel) from a finite daily allowance enforced by a database trigger. Outstanding Pieces earn heritage-style listings, Grade II, II* and I, conferred by a function over the aggregate score, the number of corroborating sightings and the doffs received, so a single self-appraisal cannot mint a Grade I.
A Piece: its Grade, its first documenter, and the appraisal aggregated across sightings.
Pieces also carry practical intelligence: accessibility flags (step-free, dog-friendly) and hazard reports (a bull in the field, deep mud) that other members confirm while they remain current.
A personal Compendium tracks each member's documented varieties, category by category, and honorific ranks climb from Wanderer through Rambler and Wayfarer towards Master of the Stile, conferred by sightings logged.
The Compendium: a field guide filled in by documenting varieties.
A member's profile: rank, tallies, streak, and counties covered.
Challenges
Capture has to work where there is no signal. Posts queue in a local SQLite store with their photo, location and appraisal, and upload when a connection returns; a terminally failed upload is set aside rather than blocking the queue behind it.
Visibility is friends-only and enforced with Postgres row-level security, so the rule holds at the database rather than in the client. Aggregates (appraisal averages, gesture counts, grades) are maintained by triggers on write.
The most instructive bug: freshly logged Pieces never appeared on the map. The map library suppresses any marker that overlaps the user's own location indicator by default, and a just-logged stile is, by definition, exactly where you're standing.
Pixel Steelworks live
Overview
An interactive pixel-art map of the Consett Iron & Steel Works in County Durham, built in 3D. Slide through nearly two centuries, from the works rising out of the moors in the 1830s, through boom and closure in 1980, to the green fields there today. Tap any of 46 buildings for its history at that point in time.
Eighteen milestones mark the timeline, from the Stanhope & Tyne Railway in 1834 to closure on 12 September 1980 and the Terris Novalis sculptures in 1996. Every date is real, checked against the archives listed in the app; the geography is a stylised tribute rather than a survey.
- Stack
- Three.js, vanilla JS, no build step
- Scene
- voxel buildings merged into vertex-coloured meshes, ~120 draw calls
- Buildings
- 46 tappable, era-by-era histories
- Timeline
- 1830 to the current year, 18 milestones
- Hosting
- Cloudflare Pages
- Licence
- MIT
1939 on the slider: the works at full production.
How it's built
Three.js, no build step, no framework. Every building is baked into one or two vertex-coloured meshes, which keeps the whole town at roughly 120 draw calls so it stays smooth on a phone.
The time slider runs on a gently warped scale: the frantic years around closure (1979 to 1984) get room to breathe while the long boom decades still read long. Building histories are written in the present tense of the selected year, so dragging the slider with a card open updates the story live, era by era, until the building fades out of existence.
1965: the works at their peak, dominating the western side of the town.
The scene is data-driven: the milestones, every building's
per-era history and the colour keyframes for the sky, the
ground and Consett's famous red dust live in one data module,
and the renderer re-derives what exists for any year. A deep
link (?year=1965) jumps straight to a moment in
time. Smaller details ride the timeline too: ore trains run
steam until 1968, sheep graze before 1840 and again after
reclamation, and a Coast-to-Coast cyclist appears from
1994.
The same ground today: returned to green, a supermarket where the plate mill stood.
Trading bot building
Overview
An autonomous quantitative trading system for US equities that iterates its own strategy. An AI strategist studies live market conditions and research, decides the posture, and hands a precise strategy to a deterministic execution engine that trades it through Alpaca. As regimes change, the system re-derives its approach without a human in the loop.
It trades a simulated account while it proves itself.
- Engine
- Python, deterministic loop measured in seconds
- Strategist
- Claude, tiered: routine checks on the small model, deep reviews on the large
- Broker / data
- Alpaca (paper account)
- Strategies
- mean reversion, momentum, VWAP reversion
- State
- SQLite event store, full decision audit trail
- Console
- FastAPI + React over one websocket
Architecture
Two speeds, deliberately separated. The execution engine runs a tight loop measured in seconds: it computes indicators locally (hand-rolled in pandas and numpy, fully unit-tested), evaluates the active strategy from a library of pure functions (mean reversion, momentum, VWAP reversion) and manages bracket orders, with no AI calls anywhere in that path. The strategist runs on a slower cadence, reading a compact digest of regime metrics, positions and recent performance, and emits a typed strategy configuration with a built-in expiry.
The engine treats that output as advice, not command: every risk field is clamped to hard ceilings the strategist cannot exceed, and with no valid configuration the engine stays flat.
Most ticks cost nothing: a local change detector decides whether consulting the model is even worth it, prompts keep a stable cacheable prefix, output is forced through a typed tool call, and heavier models are reserved for drawdowns and scheduled reviews. A daily token budget degrades the strategist to cheaper models at 80% and stops calls entirely at 100%, with the engine continuing on the last good configuration.
Safety
Every entry carries a stop from the moment it's placed. Position sizes and concurrent positions are capped, per-symbol and per-session trade counts are limited, and a daily loss circuit breaker flattens everything and halts new entries. A kill switch works three ways: a guarded button in the console, a command line, and a file the engine watches, so it still works with everything else down. On start-up the engine reconciles against the broker's authoritative state before acting, and on data loss or API errors it holds or flattens rather than opening new risk.
The backtester replays history through the same signal and risk code the live engine runs, with spread and slippage modelled, so backtest behaviour cannot quietly diverge from real behaviour.
World Cup sweepstake bot live
Overview
A Telegram bot running a private World Cup sweepstake. It follows every match in real time and posts goals, VAR reversals, results, eliminations, Golden Boot changes and the final prize breakdown to the group chat, in the voice of a bold, opinionated pundit.
Facts are sacred: scores, scorers and minutes come from a live football data API and are never invented. Opinions can be bold, but only when they're real takes circulating on X, retrieved through xAI's agent tooling and voiced as the bot's own. The factual line posts instantly; the commentary follows as a threaded reply. When England score or concede, a saved photo posts alongside. It also answers questions put to it in the chat, drawing on a personality that exists as a list of memories populated by a simulated childhood.
- Runtime
- Cloudflare Workers + one SQLite-backed Durable Object
- Facts
- football-data.org, polled every minute
- Commentary
- xAI Grok, grounded in live X reactions
- Delivery
- Telegram Bot API, send-first deduplication
- Tests
- vitest suite locking in the no-double-post guarantee
- Status
- live for the 2026 World Cup
Architecture
Serverless, on Cloudflare Workers. A single SQLite-backed Durable Object owns all state: match snapshots, deduplication keys, standings and the deferred-commentary queue. A tick runs every minute, fetches the live match data, diffs it against the previous snapshot to derive events (goals, VAR reversals, half- and full-time, eliminations, Golden Boot changes), and posts each new one to Telegram.
Delivery is send-first: an event is recorded as posted only once Telegram accepts the message, so a failed send re-fires on the next tick and nothing is lost. Grounded commentary is decoupled from the facts: the deterministic opener posts immediately and a Durable Object alarm delivers the X-grounded line when it's ready.
The tournament logic is deterministic and tested: group eliminations fire once every group game finishes, ranked by points, goal difference, goals and head-to-head, and the knockout rounds handle extra time, penalties and simultaneous owner knockouts.
Challenges
The first version kept state in an eventually consistent key-value store. It exhausted the daily write budget and occasionally double-posted when two ticks raced on a stale read. Moving state into one Durable Object fixed both: a single instance with strongly consistent storage makes overlapping ticks impossible, and a test suite locks the no-double-post guarantee in.
Mid-tournament, the platform's cron delivery silently died. The bot now carries its own watchdog: a self-re-arming Durable Object alarm that notices when the last tick has gone stale and runs the tick itself, so posting never depends on cron alone.
And the scorers endpoint occasionally serves a stale goal count for a single poll, so a new Golden Boot leader must hold for three consecutive ticks before it's announced.