DEV Community

Cover image for My Agent Built a Paid Media Storefront Before I Signed Up for Anything
Nick Bradley for Cloudinary

Posted on

My Agent Built a Paid Media Storefront Before I Signed Up for Anything

Here's a build log that would have been impossible a year ago.

  1. I told an agent to clone unshutter and get it running.
  2. It provisioned a Cloudinary Claimable Cloud, a Neon Postgres database and a Stripe sandbox, without me creating a single account.
  3. It configured the media pipeline, then ran a real test purchase end to end: watermarked preview → Stripe Checkout → signed download of the original.
  4. It handed me two claim URLs and stopped.

I claimed the environments the way you review a PR: after seeing the working result.

The unshutter gallery: watermarked previews on a warm gallery wall, originals locked behind the media layer

The storefront is just the demo. What I actually want to talk about is the provisioning.

The one step agents couldn't do

Agents don't evaluate a product the way we do. They don't read the landing page or compare the pricing table. They want to try it: spin something up, build against it, run the result, decide.

Until recently every platform had exactly one step that blocked that: creating the account. A signup form, an email confirmation, a dashboard to fish an API key out of. Ten minutes of human clicking standing in front of hours of autonomous work.

That step is gone across this whole stack:

npx @cloudinary/cloud        # Claimable Cloud → CLOUDINARY_URL in .env, claim URL, 24h
npx neon-new --yes           # Claimable Postgres → connection string, claim URL, 72h
stripe sandbox create        # anonymous sandbox → working test keys
Enter fullscreen mode Exit fullscreen mode

Three commands, no signups, and each one returns working credentials plus a claim link.

Cloudinary's version returns something like this:

CLOUD_NAME=a1b2c3d4
API_KEY=123456789012345
API_SECRET=«redacted»
CLOUDINARY_URL=cloudinary://123456789012345:«redacted»@a1b2c3d4
EXPIRES_AT=2026-08-06T08:29:07Z
CLAIM_URL=https://cloudinary.com/users/agent_email_confirmation?token=«redacted»
Enter fullscreen mode Exit fullscreen mode

It writes CLOUDINARY_URL straight into .env and the whole platform is live from that first call: uploads, transformations, AI add-ons, CDN delivery. There's no trial tier with a queue in front of it.

Two properties matter for what follows:

  • Unclaimed, it expires in 24 hours. Claiming it (email confirmation on that URL) converts it to a permanent free account, with the same cloud name, the same credentials and every uploaded asset intact.
  • Unclaimed, media delivery is locked to a public IP, by default the one the command ran from. You can name up to three at provisioning time by repeating --ip, which is enough for your laptop plus a preview host, but not enough for the open internet.

The second one is the interesting bit: the claim is the go-public switch.

The agent's build works from the IPs it was provisioned for. Claiming is the moment it can serve media to anyone, anywhere. That's why the agent stops at a working local site instead of deploying: deploy before claiming and you've published something that looks broken to every visitor but you. Claim, then deploy.

Agent provisioning diagram

What it built

unshutter (v.: to open the shutters; to let the light in) is a stock-media storefront selling photos, videos and photosets. Next.js 16, Payload CMS, Neon, Stripe, Cloudinary, MIT-licensed. Every mechanic below links to the file that implements it, so you can check my work.

The paywall is enforced by the media layer, not by hiding URLs.

  • Originals upload as restricted assets, type: private for images and type: authenticated for video (uploadOriginal.ts). The CDN refuses to deliver them unsigned.
  • With Strict Transformations on, only allowlisted transformations resolve publicly (PreviewImage.tsx is the one component allowed to render them). There's no URL you can craft to get the un-watermarked variant, and what the public can see is deliberately degraded: watermark, q_auto:low, size-capped.
  • Paying mints a ~10-minute signed download URL (buildDownloadUrl.ts). Stripe's payment record is the entitlement, checked with one live API call per visit (verifySession.ts). No user table, no entitlement table.

Videos lean on e_preview for an AI-picked five-second clip plus six filmstrip stills (videoTransformations.ts). Photosets sell a folder as one product, delivered as a signed, expiring zip that generate_archive assembles on the fly (buildPhotosetZipUrl.ts, ingest script).

A video product page with an AI-picked 5-second preview clip and a six-frame watermarked filmstrip A photoset product page: a folder of originals behind curated previews, sold as one zip

It's worth reading even if you never run it:

Read the code: github.com/cloudinary-devs/unshutter

Every one of those mechanics is configuration that has to exist inside the media account: named transformations, strict-mode allowlists, delivery types, signed archives. That's the real test of an anonymously provisioned one.

Could it actually do the work?

Provisioning an empty environment is the easy half. The question is whether an agent can stand all that up in one:

It configured the pipeline. Four named transformations created over the Admin API (the watermark chain, OG images, the AI video preview, the filmstrip stills), each flagged allowed_for_strict: true. No console, no clicking.

It verified the security model instead of assuming it. Uploaded test media, then requested a raw original URL unsigned and confirmed the 401. That check matters: if Strict Transformations happens to be off, the watermark is bypassable and everything above is theatre. So the agent proves it and reports what it found.

It ran the money path. Browse → cart → Stripe Checkout with 4242 4242 4242 4242 → webhook → /download → signed URL → un-watermarked original on disk.

Then it stopped and told me what existed and what needed claiming.

All of it ran against live infrastructure rather than mocks. When it needed current facts rather than whatever was in its training data, it read Cloudinary's agent docs and machine-readable llms.txt, the same entry points any agent can load.

Where the human went

Before Now
Get three live environments ✋ three signup forms, dashboard key hunts 🤖 three commands, no accounts
Write the credentials into .env ✋ copy-paste 🤖
Configure the media pipeline ✋ console clicking 🤖 Admin API
Verify the paywall holds ✋ hope 🤖 tested, reported
Own the environments first, before a line of code ran last, confirm an email, ≤24h / ≤72h
Deploy it ✋ your account, your call
Decide it's real money time

Read that middle row on its own: the email confirmation didn't disappear, it just stopped being step zero. Same click, opposite end of the process. By the time you make it, you're confirming something you've already watched work.

The human touches didn't only shrink, they moved to after the work, where they feel like decisions rather than chores. What's left is ownership, infrastructure and judgment, which is roughly what should need a person anyway.

Things that bit us

None of this was completely frictionless but the agent was able to route around it. Some things worth knowing before you try it:

Already have a Cloudinary account to use? You don't have to provision a Claimable Cloud (but you might want to)

Claimable Clouds make it easy to provision a working environment, but claiming one means confirming an email address. If that address already belongs to a Cloudinary account, the claim page rejects it. So a claimable cloud you provision for real work can't be folded into the account you already have.

If you're already a customer, just hand the agent credentials from your own account.

The IP lock surprises you at deploy time

An unclaimed cloud delivers media only to the IPs it was provisioned for, by default just the machine that ran the command. Locally everything is perfect; deploy it and the site is fine for you and broken for everyone else. You can allow up to three addresses at provisioning time by repeating --ip (handy for a laptop plus a preview host, and requester_ip is accepted as a literal), but going properly public means claiming, which lifts the restriction entirely. So: claim before you deploy, and if you're scripting this, have your agent report the claim URL loudly rather than burying it in a summary.

e_preview can't generate on the fly

Request it in a plain delivery URL and you get an HTTP 423 until the derived asset exists. It has to be eager-generated at upload with eager_async: true, plus a signature-verified webhook to flip a previewReady flag when Cloudinary finishes. Also: the source needs to be longer than the excerpt, so a 3-second clip fails silently.

Cloudinary can't POST a webhook to localhost

Which means the "preview ready" callback never lands during local development. Either tunnel (ngrok/cloudflared) or flip the flag manually; the repo does the latter as a documented dev fallback.

Two settings still have no API

Strict Transformations and the AI add-on registrations are account-posture settings you toggle in the console. The add-ons don't block anything: photo ingest retries without them and lands the image untagged. Strict mode does matter, which is why the agent tests for it rather than trusting it.

Try it

Point an agent at the repo:

Clone cloudinary-devs/unshutter and get it running locally, using claimable environments. Don't ask me for credentials, don't deploy anywhere. Give me the local URL and the claim URLs when you're done.

Or run the three commands yourself and follow the quick start.


GitHub logo cloudinary-devs / unshutter

Photography paywall storefront. An open-source showcase demonstrating Cloudinary in a modern full-stack app - Cloudinary + Next.js 16 + Payload CMS + Neon + Stripe + Vercel

unshutter

unshutter (v.): to open the shutters; to let the light in.

A stock-media storefront an AI agent can take from git clone to a running, verified store — media, database, payments — without a single human signup. Every platform in the stack provisions claim-later environments (npx @cloudinary/cloud, npx neon-new, stripe sandbox create), so the human touches move to where they belong: claiming something that already works. Paste the Agent quick start into your agent and watch.

Built on Next.js 16 + Payload CMS + Neon + Stripe + Vercel, with Cloudinary as the media layer. Sibling to kickoff-cards (Cloudinary + Supabase).

The unshutter gallery — watermarked previews on a warm gallery wall, originals locked behind the media layer

Three product shapes:

  • Photos — browse watermarked, heavily compressed Previews; paying unshutters the image with a time-limited signed link to the full-size Original
  • Videos — an AI-selected 5-second watermarked, low-res preview clip (Cloudinary e_preview) plus six auto-extracted watermarked stills; purchase…





The repo ships the guardrails that made agent-building work on a stack this fiddly: AGENTS.md working agreements, a provisioning walkthrough with every human touch called out, and the verified platform facts the implementation rests on. Sibling project, same idea with Supabase: Kickoff Cards.

Further reading: Claimable Cloud provisioning · Agents, start here


Most of the media plumbing is the agent's job now. What was left for me was deciding whether to keep what it built.

Cloudinary ❤️ developers
Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today.
👉 Create your free account

Top comments (0)