Get in touch
All work

Case study · SaaS, data and applied AI

Marketing Window

Every ad research tool stops at the brief. I built one that keeps going until the ad is made.

Solo build, live at marketingwindow.com. A library of competitors’ ads, an AI layer that reads them, and a studio you can order your own version from. Researched, designed and built by me.

Client
Personal project
Role
Founder. Research, product design, pricing, the data pipeline and the build.
Period
Sep–Oct 2026
  • Next.js
  • TypeScript
  • Supabase
  • OpenAI
  • Trigger.dev
  • Cloudflare R2

Finding the ad is the easy part

Meta publishes every ad running on Facebook and Instagram in a free, public library. It is one of the best research tools in marketing, and it was built for transparency rather than for marketers.

It tells you an ad exists. It does not tell you which of a brand’s forty ads is the one they keep paying for, what those ads have in common, or how to get one like it made for your own product.

The paid research tools I looked at answer the first two questions well. Then they hand you a brief and stop. The expensive part, getting the ad made, is still somebody else’s problem.

So Marketing Window does both halves: a library for finding what is working, and a studio that makes your version of it.

Three things the public library will not tell you

None of these are bugs. They are what you get from a tool that was never meant for this job.

  1. Nobody publishes what an ad spent. Outside the EU and political ads, there is no spend and no result. So “which ad is working” has to be inferred from what the brand keeps doing, not read off a number.

  2. The evidence expires. The image and video links in an ad library stop working within days. A swipe file built on those links is a folder of broken thumbnails a month later.

  3. A brief is not an ad. Finding a winner is the quick part. Getting your own version made means a shoot, a freelancer or an agency, and usually weeks.

See, save, brief, make

Marketing Window is one loop, and its last step is the one the research tools leave out.

Search any brand and see every ad it is running, ranked by what has lasted. Save the ones worth learning from into boards and shortlists. Turn a shortlist into a brief. Then send the brief to the Studio, which makes CGI renders, CGI video and video ads for your product, with a price and a delivery date up front and proofs to approve along the way.

The button I care about most is on every ad: Make this for my product. It turns someone else’s ad into a pre-filled brief and a priced order.

The whole product is one loop. Each step hands the next one something it can use.

See

Every live ad from any brand, sorted by how long it has run and how hard the brand keeps iterating on it.

See → Save → Brief → Make → See again

  1. See. Every live ad from any brand, sorted by how long it has run and how hard the brand keeps iterating on it.
  2. Save. Favourites, boards and shortlists, with the media stored on our side so a saved ad outlives the original.
  3. Brief. A shortlist becomes a structured brief, with the references attached and an AI assistant for hooks and scripts.
  4. Make. The brief becomes a Studio order: a price, a delivery date, proofs to approve and the files in your library.
The live Studio menu on the pricing page: CGI render services listed with their lead times and prices
The Studio menu: every service with its lead time and its price, all read from one config file

One app, one database, and the jobs behind them

I built it solo, with AI coding agents doing the typing while I made the calls. The first commit is dated 28 September 2026. By 2 October the library, the customer app and the Studio’s operations portal were all merged into the live site.

It is one Next.js app on Vercel, written in TypeScript: the marketing site, the customer app, the operations portal and a no-login approval portal for agencies’ clients, all on one domain. Postgres on Supabase holds everything. Media lives in Cloudflare R2. Anything slow runs as a Trigger.dev job, never inside a page request, and every AI call goes to OpenAI with the model names kept in one config file.

The search box is the part people see. Most of the work is making ad data, which rots, behave like data that lasts.
  1. Never hotlink. Every image and video is downloaded when an ad is collected, stored under its SHA-256 hash, and compared by perceptual hash, so the same creative cropped twice is kept once. The raw API response is archived as well, so a parsing mistake is fixed by replaying history rather than collecting it again.

  2. Finding the right brand is harder than searching for it. The data vendor’s page search returns fifteen results and missed Lush entirely. So the app searches what was typed, the name with the country, and a few AI-suggested variants, then ranks what it finds by whole-word match, audience size on a log scale, verification and Instagram handle. A person picks the page, and a match only counts as verified above 0.9.

  3. Every job has a budget and an off switch. Collection, media and AI each check a database kill switch and a daily budget before they spend anything. A bad API key pauses that vendor for every job at once. A health check runs daily and fails loudly if credits run low, more than five per cent of collections fail, or a kill switch has been left on.

  4. Security lives in the database. Row-level security is on every table, every policy has a pgTAP test, and CI fails if a policy is added without one. Studio orders change status only through dedicated commands, and a trigger refuses any other route, so no page can mark an order delivered by mistake.

  5. Money and dates in one place. Prices come from one config file, and every price, discount and delivery date is computed in one pure TypeScript package, in whole cents. Delivery dates count business days, skip South Australian and US public holidays, and pause while an order is waiting on the customer.

The size of the first build

  • First commit to last merge

    5 days

    28 Sep to 2 Oct 2026, 127 commits

  • Database migrations

    52

    One file per change

  • Row-level security tests

    17

    CI fails on an untested policy

  • Background tasks

    11

    Collection, media, AI, email, Studio

Counted from the Marketing Window repository at its last merge, 2 October 2026: the git history, supabase/migrations, supabase/tests and the task list in packages/jobs. These measure the size of the build, not a result. Nothing on this page is a usage figure.

The jobs that keep the library current

The part nobody sees. Nothing slow runs inside a page request, and every job that spends money checks its budget before it starts. These are the background tasks that matter most, and what each one is for.

TaskRunsWhat it does
DispatchHourlyQueues every brand due a refresh
CollectOn demandOne brand, every country, media stored
LookupFrom a searchA brand someone is waiting on
EnrichHourlyTags, transcripts, embeddings, summaries
Media retryEvery 3 hoursRebuilds failed media from the archive
Vendor healthDailyCredits, failure rate, kill switches
UpkeepHourlyAlerts, proof reminders, the delivery clock
DigestMondaysThe weekly email, sent exactly once

The Trigger.dev task list in the Marketing Window repository (packages/jobs) as of 2 October 2026. This is the design rather than a measurement.

AI that reads the ads, and is told what it may not say

The library is useful on its own. The AI layer is what makes it faster than scrolling.

Every ad that matters gets a transcript, a set of tags (hook, angle, offer, creative style, call to action) and an embedding. Every brand gets a written read of what it is testing: the recurring play, the hooks that last, its creative mix and three plays worth trying this week. The model does the reading. It does not do the ranking.

  1. Structured, or it does not ship. Tags come back as strict JSON against a schema and are validated again in code before anything is stored. A creative style the schema does not know is a failed call, not a new category.

  2. Told what it may not say. The prompt allows only what is visible or audible in the ad. It never guesses spend, sales or results, and never identifies a person, only whether one appears. On a product built from other companies’ ads, those two rules are the line between research and a liability.

  3. Grounded before it is shown. When onboarding suggests competitors, a suggestion only appears if a real Meta page matches it. A plausible brand name with nothing behind it is dropped, not shown.

  4. Budgeted like any other vendor. Every call reserves from a daily budget first, a brand someone is waiting on has its strongest ads read first, and one database flag stops all of it.

The embeddings are stored and indexed, but search does not use them yet. Keyword search shipped first. The vectors are there so that searching by meaning becomes a change to one query rather than a backfill of every ad in the library.

Signal, the 0 to 100 score that ranks the ads, is a formula on purpose: 60% how long an ad has run, 20% how many variants the brand made of it, 10% relaunches, 10% where it sits among the brand’s own ads. Everywhere it appears, it is labelled an estimate and not spend. The one number on the page that drives decisions should be one I can explain in a sentence.

The AI breakdown on the live Sleeping Duck brand page: a written read of its ads, the hooks that last, and a creative mix chart
The AI read on a live brand page: what the brand keeps testing, the hooks that last, and its creative mix

What this page cannot show you

The product does not take payments yet, and that limits what this page can claim.

Payments are the next phase. Until they are switched on, every Studio order is a test order, numbered from MW-90001 so it can never be mistaken for a real one. So there is no revenue here, no conversion rate and no customer quote, and I would rather say so than let a screenshot imply otherwise.

Signal is the other gap. The weights are my judgement of what lasting ads have in common, not a fit against real spend, because nobody outside the platform has the spend to fit against. It ranks ads the way I would rank them by eye. Whether it predicts what works is a question I can only answer once orders come back with results attached.

If you want to test a claim on this page, test a design call: why media is stored by its hash, why an order can only change status through a command, why Signal is a formula and not a model.

What I took from it

Morning Metrics taught me that most reporting problems are definition problems. Marketing Window taught me the same thing about building with AI agents.

The agents write code quickly. What decides whether it holds together is the decisions file: what the product is, what it will not do, and which call was made on which date.

Every decision on this build was dated and written down before the code that depended on it, from which vendor collects the ads to why the third Studio service is called Video Ads and never AI video. That file is how one person kept a library, a customer app and an operations portal consistent across five days of building.

The live Sleeping Duck brand page on a phone: live ads, longest running ad and top format
The AI read on a phone: what Sleeping Duck is testing, in plain sentences
The same brand page on a phone, and the AI read underneath it