Product engineering · Automation · Data

I build products end to end, and automate the work behind them.

I run Homads, a subscription-based listing site for mid-term rentals, live in 700+ US cities. I do the design, the build, and everything after launch. During the day I build internal automations and AI agents for an enterprise automation team. A lot of my work sits in the middle layer: billing, data pipelines, and the process around them.

Available for freelance and consulting work

Building products

The marketplace app, the marketing site, subscription billing, search and the content pipeline — through to deploying it.

Automating operations

Requirements gathering with the people who do the work, then the automations, AI agents and reporting that come out of it.

Working with data

Event tracking that actually arrives, daily reconciliation between systems, and reporting people can act on.

Selected work

Homads is my own company, so those projects are open — live links and real numbers. The employer work is described without internal detail.

Homads — a subscription listing site for mid-term rentals

Founder & builder

A listing site for mid-term rentals — furnished stays of 30 days or more. Hosts list free and pay a subscription to reply to renters, with no commission taken on a booking. I build and run all of it: the app itself, the public website and blog, the billing, the search, the tracking and the content.

700+
US cities
1,000+
listing owners
41,000
member host community
2,826
indexed URLs
Next.jsReactVercel Bubble.ioSupabaseStripe MixpanelNode
More detail

The pieces

  • The marketplace app — where hosts list their properties and renters search, browse and message them. Built on Bubble.
  • The marketing site and blog — everything you see before you sign in, built in Next.js. The app itself acts as the CMS behind the blog.
  • Search — city and map search runs against a read-only Supabase Postgres mirror of the listing data, so heavy search traffic doesn't slow the main app down for everyone else.
  • Billing — Stripe subscriptions across three plan tiers, plus the sync layer that keeps Stripe and the app agreeing with each other.
  • Tracking — event analytics that survives ad-blockers, reconciled against the payment records every day.

Recent work

  • Upgraded the site from Next.js 14 to 16, including the runtime change that came with it.
  • Fixed the canonical tags and link-preview images, and redirected the old URL scheme so no page was left orphaned. The sitemap now covers 2,826 pages, 602 of them city pages.
  • Found that our own crawler protection was returning errors to link unfurlers — so pasting a Homads link into Slack or a text message showed a blank box instead of the property. Exempted them, and added a unit test so it can't come back.

Rebuilding self-serve subscription billing

Diagnosis & build

Subscribers couldn't reliably manage or cancel their own plans. It turned out to be three separate problems in three different places, which is part of why it had been hard to pin down.

Stripe APINext.js API routes Bubble workflowsWebhooks
Read the case study

The problem

The account page had a cancel button and a Manage Billing button. For most people, neither one did anything — and the code looked perfectly reasonable when you read it.

What was going wrong

  • The cancel button never appeared. The app checked for a status of "Active" with a capital A; Stripe writes it lowercase. A case-sensitivity mismatch, so the condition was never true and the button stayed hidden from everyone.
  • Manage Billing looked people up by the wrong key. It resolved each customer by a Stripe customer ID that most accounts had never been given — older accounts predate it and carry a legacy identifier in a different format entirely.
  • Cancelling could accidentally renew you. Cancelling an annual plan that was already past due scheduled it to end at the end of the term — which handed that account another full year.

What I did

  • Normalised the case on the status check, and widened the condition to cover past-due accounts as well as active ones.
  • Rewrote the billing-portal endpoint to resolve the customer from their subscription record instead, which works no matter when the account was created.
  • Added an immediate-cancel path for past-due subscriptions.
  • Wired up the API connector and the app-side workflows, deployed, and tested end to end on a live account.

Result. Subscribers can cancel and manage their billing without contacting support, and the renewal path that was costing us money is closed.

Looking back

The capital letter was the easy part. The real problem was that three systems each had their own idea of what counted as "this customer," and nothing made them agree with each other. Picking one piece of information that every account definitely has, and looking everything up from that, is what actually fixed it.

A data-grounded city content programme

Research, pipeline & publishing

23 city guides, each built from our own listing data and that city's actual rental tax rules. The harder question was which cities deserved a guide at all.

23
guides published
602
city pages on the site
29
cities with real depth, of 700+
Next.jsData APINode PlaywrightProgrammatic SEO
Read the case study

Why not 700 guides

Being in 700+ cities looks like an easy win for search traffic: write a page for each one. I checked the numbers first. Half the cities on the site have a single listing, and only 29 have ten or more. Writing all of them would have produced around 670 pages with nothing in them, which hurts a site rather than helping it.

Counting the listings correctly

Working out how many listings a city actually has turned out to be its own job, and there were two traps in it.

  • Listings for one metro are filed under several different location strings, so grouping on any single one undercounts — in a few cities by more than 30%.
  • The account that created a listing is often the migration tool that imported it, not the real host. Grouping on it makes a handful of accounts look like they own half the city.

Both mistakes put wrong numbers into a draft before I caught them, so the process now handles each one deliberately rather than hoping.

What's in each guide

  • Real prices, supply and ownership figures, calculated from live listings the day it's published.
  • That city's actual tax rules, with the law cited. The question hosts have is how long a stay has to be before it stops being taxed like a hotel, and the answers are all over the place — one city decides it by how you bill the guest rather than how long they stay, and another taxes 31 nights but not 32.
  • Something specific about the neighbourhoods, found by grouping listings by street. That turned out to be the most reliable way to have something real to say about a city.

How it's built

  • A query that pulls each city's merged, corrected figures straight from the live data.
  • A generator that renders a branded header image per city from a JSON input.
  • A link between every city search page and its guide, so publishing a new city is a one-line change rather than a project.

Result. 23 of 23 published and checked, each with its own data and header image, and every city page linking through to its guide.

Group Watch — an AI listening pipeline

Design & build

Homads runs a 41,000-member host community. It's full of useful signals and the occasional complaint that needs a fast answer, and reading it by hand doesn't scale. This watches it, sorts what matters, and drafts a reply.

NodeClaude APIApify RenderSlack APIWebhooks
Read the case study

The constraint that shaped it

The obvious way to build this is to have software log in as us and read the group. That risks the company's account being shut down, and losing the community account is not worth the convenience. So everything here reads only what's already public, without logging in as anyone. The trade is that posts reach us a few hours later, which is fine.

How it works

  • A scheduled collector reads the group's public posts three times a day and sends them to an ingest endpoint on a small service.
  • Claude classifies each post into one of five buckets: buying intent, a pain point we solve, a competitor mention, a reputation risk, or a direct mention of us. Generic "does anyone know a property manager" posts get dropped rather than passed on, because they weren't turning into anything.
  • Each one lands in a Slack channel picked by how urgent it is, with a reply already drafted in the right tone — careful and service-like for complaints, peer-to-peer for everything else.

Result. Runs for a few dollars a month. A vendor quoted around $1,000/month for something comparable.

What's still unproven

The whole thing rests on an assumption I hadn't tested: that the conversations worth catching happen in the comments rather than in the original posts. Rather than pay for the expensive option on a hunch, I set up a one-week measurement to find out, and wrote down in advance what each possible answer would mean.

Making the numbers reliable

Audit & build

Our website analytics and our payment records disagreed about how much money we'd made, which meant neither could be used to decide anything. I fixed how the data was collected, then built something that checks it every day.

MixpanelStripe First-party proxyScheduled jobs
Read the case study

Three separate problems

  • Ad-blockers were eating the data. Our tracking calls went to a third-party domain, which ad-blockers recognise and block outright. A large share of visits were never recorded at all.
  • One person was being counted as two. The marketing site and the app were assigning the same visitor two different identities, so no funnel that crossed between them could be read correctly.
  • Purchases weren't all being recorded. The purchase event didn't fire on every checkout path, so the revenue picture wasn't just noisy — it was structurally incomplete.

What I did

Moved the tracking behind a first-party proxy, so the calls come from our own domain and ad-blockers leave them alone. Resolved the identity split between the two sites. Then built a daily reconciliation job that compares what we tracked against what Stripe actually took, and flags any divergence.

That last part is the one that matters. Tracking breaks quietly and you usually find out months later, when the historical numbers are already useless. Now it shows up the next day.

Result. One clear rule: the payment system is the truth about money, analytics is the truth about behaviour, and a daily check proves they still agree.

A habit that came out of this

I now check how often something normally happens before treating a number as a problem. If a sign-up channel averages one person every four days, a week of zeroes is completely ordinary. A few of my own confident conclusions didn't survive that check, so it became a step I always do rather than one I remember to do.

Hexis — automotive dealership CRM platform

Ethos Group · prior team

Hexis is Ethos Group's software for car dealerships — the sales floor, deal pricing, inventory and lead handling, all behind one login. I worked on both the front end and the back end: the connections to outside data providers, and a share of the platform's second-generation rebuild. It's licensed software, so I can't hand out a login; the company markets it publicly and their site is the best look at it.

Full-stackAPI integration Bubble.ioCRM Enterprise / B2BAutomotive
My role

What I worked on

  • Third-party data integrations. I built the API integrations against an external automotive data and valuation provider, including the feed behind the manufacturer incentives the platform pulls in automatically.
  • The second-generation rebuild. I rebuilt portions of the platform's next major version, working on both the front end and the back end.

The rebates problem

Manufacturer rebates depend on the exact trim of one specific car, and the figure goes straight onto the sheet the customer is looking at while they decide. It has to be right, not close.

Ours weren't. The provider's payload was deeply nested — several hundred nested items — and the incentives the platform surfaced didn't reliably match the make and model they belonged to.

I mapped the whole payload — every level of nesting, rather than only the fields that looked relevant — and rebuilt the mapping from that. Once the shape of the data was fully understood, the cases that had been landing on the wrong vehicle were easy to spot.

Result. Incentives display correctly for each make and model.

Why it's here

Connecting two systems together is a good part of what I do. Each one makes sense on its own, and the work is getting them to agree on what a single record even is — the same underlying problem as the billing work above, in a completely different industry.

Internal automation and AI agents

Ethos Group · current

I'm on a team that builds bots and AI agents for other departments — software that takes over the repetitive parts of someone's job. The systems are internal, so this describes the shape of the work rather than the systems themselves.

Power AutomateCopilot Studio Power BISharePoint Azure DevOpsProcess design
What this work involves

Where I sit

I do both halves — I sit down with the department to work out what they actually need, write up how the process should run, and then build it. The first half is usually where the value is. People tend to ask for the solution they've already pictured, and the job is finding the problem underneath it. One project came to me as "standardise this recurring meeting agenda" and, after one conversation, turned out to be "the numbers for that meeting live in eleven different places and nobody can see them by region, team or person" — a different and far more useful thing to build.

What I do

  • Requirements and process design — discovery with people who don't work in technology, turned into capability-level process flows anyone can review.
  • Building the automations — multi-connector flows spanning task tracking, document storage, the staff directory and chat. The unglamorous parts matter here: they run on a service account rather than my personal one, so they keep working when someone leaves, and shared connections plus run-only access let a team use them without filing a ticket every time.
  • Building AI agents in Copilot Studio that take someone's own data, validate it, turn it into a structured document, capture what happened afterwards, and produce the follow-up artifact for the level above.
  • Building the reporting on top — multi-page Power BI reports designed to a spec rather than left on defaults, including the data model behind them, the scheduled refresh, and the workspace permissions.

A few decisions from this work

  • Replaced a report-triggered automation with a plain link into the tool that already did the job, because the platform's refresh ceiling meant the fancier version could never have shown current data anyway.
  • Moved a scheduled data refresh so it runs after the job that feeds it, instead of at the same time.
  • Held off migrating a flow's connections onto the service account until it had the right licence, rather than doing half the move and leaving it in two states at once.

Something you can actually try

The systems above are internal, so instead I rebuilt one in a world I made up: a review agent for a fictional coffee chain. Same shape as the real work — messy export in, validated, standardised document out, meeting captured, summary routed on — on entirely invented data, built on my own time.

The part worth watching is what it does before it produces anything. It checks the file, and the findings it leads with aren't the obvious ones. A labour cost of 187% is easy; anyone reviewing a spreadsheet catches that. The dangerous ones are a store filed under two different IDs, a duplicated month whose two copies disagree, and three sales figures written with a currency symbol so they total as zero rather than raising an error. Each of those produces a number that looks entirely reasonable and is wrong.

Where a defect has one correct fix it applies it and says so. Where only the manager can know — which of two conflicting rows is right, which district an unlabelled store belongs to — it stops and asks. A wrong number that reaches a review is worse than an awkward message.

Stack

Tools I've used on production work.

Product & web

  • Next.js / React
  • Bubble.io
  • TypeScript
  • Node
  • Vercel
  • Playwright

Data

  • Postgres / Supabase
  • Power BI / DAX
  • Mixpanel
  • REST & webhooks
  • Python

Automation & AI

  • Power Automate
  • Copilot Studio
  • Claude API
  • Agent design
  • Apify

Operations

  • Stripe
  • Slack API
  • SharePoint
  • Azure DevOps
  • Git / GitHub
  • Render

Get in touch

I'm available for freelance and consulting work — marketplace and subscription products, billing and payments, process automation, and analytics.