Commercial · Product · Engineering

I ran the commercial side for twenty years.
Now I build the systems too.

I led business development, sales, strategy and product in senior roles across disruptive digital businesses — spanning startups, publicly quoted and private lottery groups, and boutique City insurtech and trading firms.

For the last three years, I’ve been building software full-time. Mostly software built around AI — agents, automated pipelines, document processing, generative media, and the products and infrastructure around them. Frontend to infrastructure. End to end.

The combination is the point. I think commercially as well as technically — value, cost, constraints and outcomes are part of the engineering problem, not something handed down with the specification. Then I build the system end to end.

Weeks, not quarters.

Open to work · Based in Spain · UK & Europe

The questions come in an order.

Case Studies

Different problems. Same approach.

Each started with the problem, not the technology. Work out what needs to happen, design the system around it, then build the thing end to end.

Two were commissioned by clients and are live with real users. The others started with problems I identified myself. Client references available on request.

Bene Esse

Pre-patient growth platform for a private healthcare practice
Live with a paying clientCommissioned
Client: Bene EsseBrief: no line of sight between ad spend and patientsStatus: live, taking paid traffic

A platform owning the whole path from ad click to sold treatment — landing pages, enquiry, qualification call, booking, sale — handing off to the practice management system at the moment someone becomes a patient. No medical record ever lives in it.

The funnel, end to end

The platform

Landing page management with lifecycle states

Draft → Scheduled → Live → Retire. A page can't go live without a campaign and an ad ID attached — so an untrackable page can't exist.

This platform handles patient data, so every screenshot is a demo environment. All names, figures and campaigns are synthetic.

The problem
Private clinics buy ads and lose the thread. Google tells you a click happened; the practice knows a patient turned up months later; nothing joins the two. The practice was losing visibility between what it spent on advertising and the patients who eventually came through the door — so budget decisions got made on instinct, and the agency in the middle had no incentive to close the gap. Meanwhile launching a new landing page meant going back to the website provider and waiting days. The gap isn't a CRM and it isn't an ads dashboard — it's the join between spend and outcome, held with enough discipline that the number at the end is worth budgeting against.
Outcome
  • Click-through rate doubled to 14.5%, with cost per click down — last 30 days, Google Ads' own figures
  • Full attribution from keyword to sold treatment — which marketing activity produces patients, not just clicks or enquiries
  • Daily optimisation decisions from the whole funnel rather than a monthly report — live now
  • Landing pages go live in about an hour instead of days waiting on an external supplier
  • The practice dropped its external marketing agency and website builder
What I built
One system holding the entire pre-patient journey: Google Ads spend pulled in by API, a landing page builder assembling pages from reusable components, enquiry capture, a free qualification call, a booking engine and calendar written from scratch rather than embedded from a scheduling service, and the conversion to patient — the billable event, and the point of handover to Semble where the clinical record lives. Reporting joins all of it, so the practice can see which keyword, search term and page turned into sold treatment.
How I solved it
  • Attribution that survives contact with reality — spend and leads join on Google's numeric criterion IDs, never on keyword text. Rename a keyword in the console and a text join dies silently; “ADHD assessment Kent” and “adhd assessment kent” are one keyword to a human and two strings to a database. The ID arrives prefixed and is stored verbatim, stripped only at join time — what Google sent is a fact, and facts don't get edited on the way into an audit trail.
  • Three cuts of the same money that must never be summed — keyword-level spend is the complete accounting total; search-term spend is partial by design because Google withholds low-volume terms; ad-level spend is a serving attribution, not a billing split. Three collections, three grains, and the discipline never to add them. A double-counted pound destroys trust in every other number on the page.
  • Instrumented at every stage, because I built every stage — landing, scroll, calendar open, which appointment type was chosen, how long the choice took, booking, conversion. Time is captured from page mount rather than form submit, so the number is how long someone read before committing, not how fast they typed. A parent who books in ninety seconds is frightened; one who reads for eleven minutes has been persuaded — same spend, two different stories. A conventional ads agency rarely gets this view because it doesn't own the pages, calendar and booking flow.
  • The system turns the funnel data into decisions — because every stage lands in one place, the numbers can be crunched rather than guessed at. Negative keywords worth excluding, where people drop off, which pages and appointment types actually convert — surfaced daily as optimisation decisions rather than a monthly report nobody reads. A conversational interface over the same data, so the clinic owner asks a question on WhatsApp or Telegram instead of opening a dashboard, is in development on the Mastra and Telegram flow already working in Agent Peanut below.
  • An untrackable page can't exist — a landing page moves through Draft → Scheduled → Live → Retire, and it cannot go live without an offering, a campaign and at least one ad ID attached. Reporting starts counting from each page's own go-live, so a page is never credited with spend from before it existed.
  • Bookings that can't double-book — a browser can't check for a conflict and then write atomically, so every booking runs as a Firestore transaction with the conflict check inside it. Slot generation handles British Summer Time properly and refuses to boot if the runtime can't resolve Europe/London, because a build without full ICU silently returns UTC and every summer appointment would be an hour out for half the year, with no error anywhere.
  • A booking model the client can't lie about — some practices triage before the doctor, some don't. The page never says which calendar it books into; it sends only which page it is, and the server resolves the rest from a record only an admin can write. A crafted request from someone reading the page source still lands in that page's real calendar.
  • Numbers that hold up — a missing value is never zero, division by zero returns null rather than Infinity, money is integer pence throughout, and unattributed spend is surfaced with an explanation rather than quietly absorbed into a page that didn't earn it. “Spent £4,000, earned £0” is false when the truth is “we weren't tracking yet”.
  • A hard boundary at the handover — becoming a patient is terminal and billable, so undoing it is enforced server-side rather than hidden behind a button. A client who can quietly un-register a patient can quietly un-bill themselves.

Next.js 16 / React 19 / TypeScript / Tailwind v4, statically exported. GCP-native: Firestore, Auth and Cloud Functions in europe-west2, with a role-based rule set doing the multi-tenancy — admin, clinician and qualifier see different things. Resend for transactional email with RFC-5545 calendar invites. Google Ads API for the cost side. Twelve Firestore collections, every record carrying an orgId, because multi-tenancy here is a filter and never a reshape.

A cross-org partner API (/v1) exposes clinic offerings with live availability and server-to-server booking, so an aggregator can search across providers. A visits lane captures the top of the funnel without PII, giving the reporting a denominator it never had. Every lead carries an append-only timeline, so corrections are further entries rather than rewrites. The reporting epoch is derived from when pages actually went live — a React component has no birthday.

A platform-wide redaction mode obscures patient-identifying data at source, which is what makes demonstrating the system possible at all without exposing a single real record.

Currently migrating to its own infrastructure and a fully multi-tenant architecture — one deployment serving many providers, resolved per request rather than per build.

Next.js 16React 19TypeScriptTailwind v4GCPFirestoreCloud FunctionsGoogle Ads APISemble APIResendClosed-loop attributionMulti-tenant by orgIdTransactional booking

FinalEyes

Certified translation, reconstruction and review · finaleyes.co ↗
Live with usersCommissioned
Client: Copywrite ColombiaBrief: perfect translation or perfect reconstruction — never bothStatus: live, in daily use

Documents in, certified documents out — tables, boxes, fonts and layout preserved — reviewed in a translation workbench built from the ground up, with a certified translator approving every segment before it's issued.

The platform

The problem
You can have a perfect translation or a perfect reconstruction. Not both. Translate the text and the machine gives you good language in a broken document — tables collapsed, boxes misaligned, fields drifted off their labels. Translate through the document's own structure and it comes back intact, but the language is sub-standard, because the model is seeing fragments rather than sentences. The industry picks the first and pays people to rebuild the document afterwards: hours of desktop-publishing rework on every certified job. And for legal and official use the output has to be a faithful reconstruction — one dropped segment in a government form is a real-world failure.
Outcome
  • Reconstruction goes from hours to minutes — the translator reviews rather than rebuilds, on complex multi-page documents with tables
  • The client's existing machine-translation step replaced entirely by my own route and editor
  • Live and in daily use by the agency that commissioned it
  • Every action logged and audited — required for certification, and the same record is the material for improving the next job
The insight
Take the route nobody takes. Translate through XLIFF and accept the worse language — because structure is expensive for a human to fix and text is cheap, but only if you have something to compare against. So the raw translation gets generated too, never to ship, purely as a reference. Now there are two versions of the same document: one structurally perfect, one linguistically better. Reconciling them is a job an agent can do.
How I solved it
  • Lex — the review agent — reads the structurally-correct XLIFF against the raw and lifts the better wording where the improvement is unambiguous. It checks consistency: the same phrase in the same context translated the same way across the whole document, which nothing working segment-by-segment can see. It checks integrity: names, numbers, dates and reference codes carried through exactly, because those are what make a certified document legally wrong when they drift. Where it's confident it applies — reversibly, always. Where it isn't, it flags with the reasoning attached. It never quietly drops anything, so it flags generously and lets the translator decide. A job that took hours takes minutes.
  • A CAT tool, built from scratch — not a wrapper around an existing workbench. XLIFF projects, segment status workflow, side-by-side review, glossary management, the certification step and secure authenticated download. Building it rather than integrating one is what makes everything else possible: a flagged segment takes seconds to resolve rather than a hunt through a file.
  • Glossaries instead of translation memory — translation memory is fuzzy string matching. For this system, a context-aware glossary solves the problem more cleanly. Lex proposes a term, the translator accepts it with one press, and it applies forward from then on — so terminology isn't re-decided every job. Nothing enters it automatically. These are certified translations; a human owns every decision that persists.
  • The DOCX pipeline — near-perfect reconstruction — genuinely XML-native: extract, translate, reconstruct, rebuilding the file from only what was in the original, with binary assets preserved so Word never sees a corrupt document. Coordinate-based box detection reads table geometry from the XML so form fields stay grouped and land back in place. This is the primary route and the one in daily production use.
  • The PDF pipeline — reconstructing what the file doesn't tell you — a PDF carries no semantic structure, so it has to be inferred. Document AI returns the layout tree, which gets deconstructed and used to estimate typographic properties the file never stated: point size derived from bounding-box geometry, alignment from position. The document is then rebuilt with PyMuPDF, which has to hold when translated text runs longer or shorter than the source.
  • Never silently wrong — every batch retries with backoff; a permanently failed segment falls back to source text and is flagged for review, never dropped. ID mismatches between request and response are logged, not swallowed.
  • Two-level concurrency without context bleed — segments are grouped by element type so paragraphs are never mixed with table cells, groups run in parallel, and batches within each group run in parallel too. Fast, without ever handing the model a mixed soup of content it would mistranslate.

Built GCP-native throughout: a Python / Flask service on Cloud Run, Cloud Functions in TypeScript, Firestore, Auth and Storage, and Document AI for the PDF route. Translation providers sit behind one interface and are swappable config rather than architecture, so the model in use is a decision rather than a dependency. Next.js frontend.

The complex DOCX pipeline rebuilds the .docx from only the files present in the original, base64-preserving binary assets. Box detection reads table coordinates from the XML so semantic fields map 1:1 through translation, which is what lets a form come back with its fields still attached to their labels.

The two-version approach means every job produces a structurally-perfect XLIFF and a raw reference translation. Lex diffs them at several levels — wording, consistency across the document, and integrity of names, numbers and codes — then applies or flags. Both versions and every action are retained, which certification requires and which makes the whole corpus available for improving the system.

PythonFlaskGCPCloud RunCloud FunctionsDocument AI (OCR)PyMuPDFXML / DOCX reconstructionXLIFFNext.jsTypeScriptAgentic reviewHuman-in-the-loopConcurrent pipelines

TurboGaff

Automated property-video platform
Pre-launchRuns end to end
Brief: mine — property video only exists at the top of the marketInput: a listing URL, nothing elseCost: under £1 per finished film

An estate agent's listing URL goes in; a narrated, captioned, branded property film comes out. No editor, no photographer, no timeline — and zero input from the agent, because it works entirely from assets they've already published.

The output it produces

The platform

Single property menu

One property: a walkthrough film or a narrative cut, chosen per listing.

Enhancement, bounded — declutter

The problem
In the UK, only very expensive properties get video — a videographer costs more than the listing justifies. Everything below that ceiling gets stills and a paragraph. And agents have a second problem underneath the first: they have nothing to put on YouTube or social. A market's worth of listings, no content, no channel. The constraint isn't taste, it's unit cost — which makes it an engineering problem, not a creative one. Get a finished film under a pound and the market changes shape.
Outcome
  • Under £1 per finished film, all in — against a videographer's day rate
  • Runs end to end today: URL in, finished branded film out, no human touch
  • Eight film formats, including aggregate films across multiple listings
  • Every figure above measured from real renders, not projected
What I built
A platform that ingests any UK agency listing page, reconstructs the property from its photographs, writes and narrates a walkthrough, and renders a finished branded film — plus aggregate films across multiple properties, so an agent has a channel rather than a one-off. Eight film formats. No human in the loop at any stage.
How I solved it — five hard problems
  • A scraper with no per-site integrations — hundreds of UK agency sites, zero per-site code. Direct API and feed integration is the right long-term route, but a feed means a signed agreement before an agent has seen a single film. Scraping means any agent can paste a URL and watch their own property become a video in minutes — which is what actually sells it. So the collector had to work everywhere, immediately, with no per-site logic. Static fetch and headless render are unioned rather than chosen between; tabs are revealed by what the button says, never by a portal's DOM shape; three harvest channels catch gallery manifests buried in script JSON. The collector is deliberately dumb and every interpretation happens downstream.
  • A full walkthrough with no prior knowledge of the house — no floorplan, no geometry, no camera poses, no idea what's upstairs. Fifteen to forty-five photos in whatever order someone uploaded them. A blind vision pass reports only what's visibly in each photo, given no listing text — tell a model there's a principal bedroom and it will find one. A second pass sees every observation at once, clusters photos of the same physical space, and matches them to rooms the listing implies. Judge the room you're standing in, never the one you can see into. Walk order comes from a narrative skeleton, floor order, observed sightlines and upload order as a weak tie-break — which is why it reads as though someone walked the house.
  • Making static images look like motion — the first version was a per-frame homography. It looked wrong, so it was measured: 85% translation, about 4px/sec of actual perspective. Replaced with a genuine 2.5D warp — monocular depth per photo, then a per-frame backward warp displacing each pixel in proportion to how near it is. Parallax now reads clearly at 2.2% displacement where the homography needed 7%.
  • Enhancement that can't become invention — a photo can be improved: the lawn mown, clutter cleared, a grey sky lifted. What can never happen is a change to what the property is — a wall moved, a window added, a room made larger. Under UK consumer-protection rules the second is a misleading advert, so the line is enforced rather than trusted: every enhanced image is shown to a vision model alongside its original and judged on structural change alone, and anything that alters the building is rejected. The same discipline runs through the script — it can't claim a feature the agent's own copy doesn't state, and even rounding is constrained, so an approximation isn't allowed to make a property sound nearer a station than it is.
  • All of it under £1 — most edit decisions resolve deterministically in code, with model calls reserved for genuine judgement. Speech is the master clock: the script is spoken in one continuous take, so per-character timings map straight back to beats with no transcription or forced alignment, and a plan whose clips don't cover their beat is rejected before it reaches the renderer. Picture can never drift from voice, and nothing expensive runs twice.

Three tiers with a strict membrane: a Next.js front end that verifies identity and holds no secrets, Cloud Functions owning every judgement — prompts, schemas, decisions — and a Python service on Cloud Run owning the muscle: Playwright, FFmpeg, numpy. Stages advance as a chain of documents rather than a pipeline of calls, each watching for a transition into its trigger status and writing a different one, so no stage can re-fire itself. A render exceeds the 540-second function cap, so the render service owns its own terminal status and writes it back.

zod is the spine: one schema gives inferred TS types, a model-facing JSON Schema constraint, and runtime validation — drift eliminated at both ends. Invariants live in the schema rather than the docs, and job-config defaults are typed as a full Record, so adding a new film format is a compile error until someone declares its voice, music bed and graphics.

The enhancement pipeline plans per photo rather than from a fixed menu of surfaces, so it works from what's actually in the room instead of a list of nouns it was seeded with. Verification is a separate call that sees both images and is asked one question only: has the structure changed. Judging the improvement and policing the boundary are deliberately different jobs.

Disocclusion was solved by eye, not by metric. Worst-case stretch of one source pixel: none 17.7×, blur the depth map 4.4×, dilate-then-blur 5.4×. The middle option scores best and looks worse — a symmetric blur spreads the depth edge into the object, so chairs carry a gradient and bend as they move. The metric improved and the picture got worse.

Every finished edit is reviewed twice — a deterministic rig of measured thresholds, and a model asked the one thing code can't judge: does this work as a sequence. Neither sees the other's findings, neither is applied to anything, both are stored beside the film. Earned restraint: an earlier stage silently downgraded the signature camera move on every clip for weeks and nobody noticed, because a downgraded move is still a valid move.

TypeScriptNext.jsGCPCloud FunctionsPythonCloud RunFFmpegnumpy / PillowPlaywrightzodElevenLabsfal.aiMonocular depth estimationSchema-enforced contractsFail-closed multi-tenancy

SynthMedica

Synthetic patient engine for clinical training
In developmentRuns end to end
Brief: mine — textbook cases don't train diagnostic skillDomain: regulated, high-stakes

Trains clinicians against patients who lie, hide things, exaggerate or push their own agenda — while the clinical ground truth they have to reach stays exactly fixed.

The problem
Real patients are unreliable narrators — anxious, evasive, exaggerating, sometimes seeking something they won't admit to. A textbook case trains none of that. But you can't just prompt an LLM to act like a difficult patient: it drifts, it isn't reproducible, and you lose track of what the right answer even was. The hard part isn't generating a patient — it's varying behaviour without ever corrupting the correctness the trainee is being marked against.
Outcome
  • Context distorts presentation, never reality — a clean separation of a simulation's behaviour from its correctness
  • Fully reproducible from a seed, which is what makes it usable for assessment rather than just practice
  • Doctor sign-off built into the workflow — augmentation, not replacement
What I built
An eight-stage pipeline driven by a declared patient context — say, a hidden opioid-seeking agenda — that transforms how the patient presents while leaving the answer key untouched. Only two of the eight stages touch a model. The rest is deterministic.
How I solved it
  • Behavioural traits — the patient's context maps to weighted behavioural traits (manipulation skill, overtness, consistency, truth-alignment), rolled deterministically from the scenario seed. Same scenario, same patient, every time.
  • Behavioural instructions — the weighted traits become explicit instructions the chatbot obeys: when challenged, double down; escalation limit three; pressure target, medication.
  • Patient presentation — a deterministic transform changes what the patient reveals and how they present it: presentation style, ideas/concerns/expectations and symptom history. No model involved.
  • Two model calls only — reorganising what the patient volunteers versus hides, and generating delivery-tuned phrasing. The LLM is confined to the linguistic surface.
  • Two-layer validation — one check confirms the declared context is actually expressed in the output; another blocks nonsensical pairings, like opioid-seeking for a skin rash.

The pipeline runs off a Firestore trigger, not a frontend call — saving a context fires the whole chain, with guards against infinite loops (draft variants only, context must have actually changed, not already processing).

Trait definitions are pure data, the rolls are seeded, and the output is a structured set of instructions. I've since extended this to deterministically alter the patient's underlying intent, not just their surface presentation — a deeper layer of reproducible control over the same fixed ground truth.

Stack: GCP throughout — Firestore, Cloud Functions and triggers — with a Next.js frontend using reactive onSnapshot updates, and the model confined to the two enrichment calls. Validation and compatibility logic live entirely in deterministic TypeScript.

GCPCloud FunctionsFirestore triggersTypeScriptNext.jsDeterministic engineSeeded generationHuman-in-the-loop
Also built

A few of the others — the same orchestration engineering pointed at lighter domains. Two of them live and running unattended.

FightAnything

Autonomous content engine + live arena · fightanything.com ↗
Live

A prompt becomes a complete fighter; a league decides outcomes by logic, not a coin-flip; and a daily highlight broadcast assembles itself from numerous AI services with nobody in the loop.

The engine decides who wins; the LLM only writes the story of how. Every fight is stored in a vector DB so the commentary knows two fighters have met before. Signed webhook orchestration with Ed25519 verification and replay protection, and talking-head GPU pods that spin up per job and tear down — compute paid for only when used.
GCPCloud RunPythonfal.aiRunPodElevenLabsVector DB (RAG)FFmpeg

lazy.productions

Automated music-video pipeline · lazy.productions ↗
Live

One prompt becomes a finished music video — lyrics, song, synchronised visuals, final render — through a route-based pipeline orchestrating a shifting roster of media models.

A voice model sings what it reads; a transcription model writes what it hears — and when those diverge, alignment fails silently at scale. A normalisation layer bridges every case where one model's output and the next one's expected input don't match. Providers are swappable config, not architecture.
FFmpegWhisperSunoRunway / Kling / LumaFluxReplicate / fal.ai

Agent Peanut

Governed multi-tenant agent platform · agentpeanut.com ↗
In development

A platform letting a small business run its operations through one conversation.

Agents never receive external credentials or call external APIs directly. They write intent documents; privileged code validates permissions, rate limits, organisation and kill-switch state before executing anything. A zero-token compliance pipeline checks every message and escalates to a model only when a checker actually fires.
MastraTypeScriptPythonGCPCloud RunNext.js 16Telegram / WhatsApp

Stack & capabilities

The toolkit.

Claude Code inside VS Code, with ChatGPT alongside — that combination is how everything above gets built. The rest is what the problems needed.

How I build
Claude CodeVS CodeChatGPT
Languages
TypeScriptPythonJavaScriptNode.js
Cloud & infrastructure
GCPCloud RunCloud FunctionsFirestoreFirebaseDockerRunPod (self-hosted GPU)VPSVercelCloudflare
Agents & orchestration
MastraAgent architectureRoute-based pipelinesWebhook orchestrationRAGVector memory
Reliability & security
Deterministic pipelinesSchema-enforced contracts (zod)HMAC / Ed25519 verificationSSRF protectionRetry / backoff / fallbackFail-closed multi-tenancy
Vision, document & data
Vision-language modelsMonocular depth estimationGoogle Document AI (OCR)PyMuPDFXML / DOCX reconstructionXLIFFnumpy / PillowFFmpegPlaywrightBeautifulSoup
Backend & frontend
Next.jsReactFlaskFirestore triggersReactive UI (onSnapshot)
Models & providers
OpenAIClaudeGroqQwen+ essentially any LLM provider
Generative media
ElevenLabsWhisperDemucsRunwayKlingVeoLumaFluxSunoSadTalker+ most gen-AI media models
APIs, comms & payments
Google Ads APISemble APIfal.aiReplicateTelegram Bot APIWhatsApp BusinessTwilioResendStripe
A snapshot, not a ceiling. New API, unfamiliar SDK, service I've never touched: read the docs, wire it in, ship. The stack changes with the problem.

Background

Twenty years running products.
Three building them myself.

I led B2B business development at a Frankfurt-listed lottery group, building insured-lottery products for operators, sports clubs and media companies. I co-developed an insurtech platform with predictive pricing and real-time risk management for the lottery and gaming markets. I founded the UK's first online + TV bingo platform — video and audio generated and simulcast to live television in real time — and earlier co-founded a graduate recruitment business that exited to Hot Group PLC.

P&Ls, my own companies, deals with real money behind them, and products launched into regulated markets where getting it wrong is a legal problem rather than a bug report.

The conventional route is engineering first, commercial understanding later. Mine ran the other way — twenty years of commercial and product experience, then engineering. Until very recently, that route barely existed. AI-assisted development changed that.

Where I fit

Different titles.
Same underlying job.

I'm interested in roles where the problem isn't already neatly specified — where understanding the business, deciding what should be built and actually delivering it are part of the same job.

Forward-Deployed Engineer
Work directly with customers on difficult problems and build the solution through to production.
Product Engineer / AI Product Engineer
Own products and systems end to end, from problem and product decisions through to the working software.
Solutions / Implementation Engineer
Turn complex business requirements into production systems, integrations and workflows.
Technical Product / Product Leadership
Own what gets built and why, with enough engineering depth to prototype and deliver rather than stopping at the specification.
Technical Consultant / AI Consultant
Combine discovery, commercial judgement and hands-on technical delivery.
AI / Technology Lead
Find where technology can materially improve a business, then own the systems, workflows and implementation that make it happen.

The title matters less to me than the shape of the work: give me a difficult problem, enough ownership to solve it properly, and responsibility for getting the solution into the real world.

Get in touch

Let's talk about
what you're building.

If the problem is difficult and nobody has written the specification yet, that's the work I want.

Based in Spain · UK & Europe · on-site, hybrid or remote