A Reddit campaign with a hero image, in one conversation
One prompt, six tool calls and a paused Reddit image ad — plus the promoted post we shipped that read like an ad, and the change that stopped the next one.

Our last Reddit post ended on a claim we should have shown rather than stated: that shipping a campaign is now one conversation. So here is the conversation.
The thing being built is a promoted image post — a title, a picture, a button — pointed at one landing page, in three communities. The picture is the part most people skip, and it is the part Reddit gives the most room to, so it gets its own name here: the hero.
Everything below runs through Growomat's hosted MCP connector, which exposes the same public API our own dashboard uses as a set of tools. Any MCP client can drive it; we use Claude. Nothing in the flow is Reddit-specific until the very last step, which is rather the point.
The prompt
This is the whole input. It is worth reading closely, because almost every line of it exists to close a decision that would otherwise be made for you:
Build a Reddit campaign for growomat.com/run-ads-from-claude, and stop
before anything reaches Reddit.
Who I want: someone who already builds with MCP servers and already
spends money on ads. Not a beginner audience.
Campaign "Reddit · MCP launch", daily budget <your number>, maximize
clicks, and scope it to reddit-ads only — I do not want this on any
other platform.
Ad group: communities mcp, ClaudeAI, SaaS. Bare names, no r/ prefix.
Ad: an image post. Use the hero I uploaded as the marketing image, call
to action "Learn More". Write the title yourself — sentence case,
60 to 120 characters, something a person would actually post.
Then validate the export and show me every finding. Do not sync.
Two of those lines are load-bearing in a way that is not obvious.
"Scope it to reddit-ads only." A campaign carries a per-platform budget split, and absent is not zero. A campaign with no split exports to every connected platform at the full budget, so a campaign you thought of as one number becomes that number on Google Ads, on Microsoft Advertising, on Reddit Ads and on Meta Ads at the same time. It is the single most expensive default in the product, and the only defence is saying which platform you meant.
"Bare names, no r/ prefix." Reddit accepts ["mcp", "ClaudeAI"] and refuses
["r/mcp", "r/ClaudeAI"] with 400 You cannot set invalid communities. We
learned that one the way everybody learns it.
What came back
Six calls, in order, each one returning the object it created:
create_campaign → PAUSED · budget scoped to reddit-ads only
create_ad_group → communities: [mcp, ClaudeAI, SaaS]
create_asset → IMAGE · 1200×628 · registered by URL
create_ad → postType: "IMAGE" · cta: "Learn More"
attach_asset_to_ad → scope: AD · role: MARKETING_IMAGE
validate_export → valid: true · 1 finding (degraded)
Everything a platform has and the shared model does not — the communities, the post type, the call to action — rides in one per-platform extension map on the object it belongs to, keyed by platform id. Nothing above branches on Reddit until the adapter does.
The campaign now exists in the product, complete, editable, and connected to nothing. That last property is the design: a draft that costs nothing to throw away is a draft you will actually redo.
An image post has nowhere to put your body copy

A promoted post is a post, and Reddit's post types are not interchangeable skins over the same fields. They carry different things.
An image post is a title and a picture. There is no body. Whatever argument you wanted to make has to survive in one sentence above the image, and the click comes from a call-to-action button attached to the creative.
A text post is the mirror image. It takes as much body as you like, and it gets no button at all — Reddit refuses a click URL on a free-form post. Send one anyway and you have published an ad that points nowhere. The honest best a text ad can do is carry its own link inside the body as a named markdown link, which is what ours now does; the click becomes a tap on a link in the text, and the tracking survives.
Because the two formats destroy different halves of your copy, the format has to
be a choice, not a guess. Ours used to be inferred — if an image was attached,
you got an image post — and that inference quietly deleted the post text of
everyone who attached a picture to a piece of writing. It is a field now
(postType), and the old inference only survives for campaigns authored before
the field existed, so nothing changed underneath anybody.
The finding in the trace above comes from the same place. Declare an image post, forget the picture, and validation says so:
REDDIT_DECLARED_IMAGE_NO_MEDIA · degraded · _native.postType
"This ad is set to run as an image post, but no image reached it.
It will export as a text post."
Warned, not blocked. The ad does export — what is lost is the shape you asked for, and a validator that can name that is more use than one that refuses.
The hero has one job
The image rides on the ad as a MARKETING_IMAGE: landscape, 1.91:1, 1200×628.
Attach one at a different ratio and the square and portrait variants can be
derived from it by cropping; the reverse does not work, so the landscape one is
the one to have.
There is no separate upload step. Reddit consumes media inside the post creation job, by URL, so registering the asset is genuinely just handing over a link and letting the sync fetch the bytes when it needs them.
What we ask an image for has narrowed a lot with practice:
A single subject, photographed or rendered, filling a 1.91:1 frame.
No text in the image — the title carries every word, and Reddit
crops and compresses the picture on mobile until small type is
unreadable anyway.
No logo lock-up, no badge, no border, no stock handshake.
It should look like something a person would have posted, and it
should still read at thumbnail size in a scrolling feed.
That last line is the whole brief. A feed is a scroll, and the hero's job is to survive it — not to explain the product, which is what the title is for.
How we got ours great
Here is the first promoted post this product ever produced, in full. The title was:
Integrate MCP Easily
Twenty characters, in a slot that takes three hundred. Title Case. Below it, a body assembled out of search-ad description fragments, because that is what the product had lying around. The verdict, from the person whose money it was, was that the Reddit copy was crap.
They were right, and it was not the model's fault. Nothing had ever told it what a Reddit post is. It was handed the slots of a search ad — three headlines under thirty characters, two descriptions under ninety — and asked to fill them, and it filled them correctly. Then a translator moved the results into a post, where "correct" reads as an advert wearing a costume.

So the fix was not a better model or a longer prompt. It was telling the drafting step what platform it was writing for — and putting that where it cannot drift, which is the same file the platform's field limits live in:
copyStyle: { // abridged from the file
post: {
persona: 'a Reddit-native copywriter who writes the way'
+ ' redditors actually post',
rules: [
'Write like a person telling other people something'
+ ' useful, not like a brand announcing itself.',
'Ground every specific in the business context given'
+ ' below — the website, the audience, the value props.',
'Plain language, short sentences, no corporate register.',
'Never reuse a search headline as a post title, and'
+ ' never assemble the body out of search fragments.',
],
avoid: [
'hashtags', 'ALL CAPS', 'emoji spam',
'marketing superlatives ("revolutionary", "best-in-class")',
'Title-Case Keyword Slabs', 'exclamation-mark spam',
'anything that reads like an ad wrote it',
],
slots: {
postTitle: { targetLength: { min: 60, max: 120 }, … },
postBody: { targetLength: { min: 400, max: 900 }, … },
},
},
},
Three things about that block are deliberate.
The target lengths are not the limits. Reddit's title cap is 300 characters and its body cap is far higher than anything we would ever write; those numbers are enforced elsewhere, as truncation rules. The 60–120 band is a quality target, and it exists because a 20-character title in a 300-character slot is how a reader can tell, at a glance and without reading it, that a brand wrote this.
The avoid list is specific enough to fail. "Be authentic" is not an instruction. "Never assemble the body out of search description fragments" names the exact thing that went wrong, so it can be checked.
It lives in the platform's capability manifest. Each platform we support declares what it can do in one versioned file — its field limits, its bidding strategies, its accepted call-to-action values, and now how its copy should read. The drafting step reads that file rather than knowing about Reddit, which is why the next platform is a manifest rather than a fork of the copy code. And because a manifest that claims something its adapter cannot do fails our conformance suite, that file is checked by CI rather than by hope.
Nothing goes live until you say so
Everything to this point has touched no ad platform at all. Publishing is two more calls, and both of them are gates:
preview_sync_plan(campaign_ids: […], platforms: ["reddit-ads"])
→ what would be created, updated or removed · planHash
trigger_sync(campaign_ids: […], plan_hash: "<that hash>")
→ started · poll get_sync_status until terminal
Passing the hash back is what turns "preview then push" into a real safety
gate: if anything changed between the two calls, the sync fails with
SYNC_PLAN_DRIFT rather than pushing a plan you never saw. The scope behaves the
same way — a plan that would touch a campaign outside the ids you named is
rejected, not silently widened.
What lands on Reddit is paused. You look at it in Reddit's own interface, with
its own preview, and you enable it there or you go back and edit. And then read
the delivery status rather than the response code, because the most expensive
lesson from our own integration was that a 200 means Reddit accepted your
request, not that anything is running.
One more thing that is part of the creative, not separate from it: leave the comments on and answer them. Comments are Reddit's trust surface. An ignored thread under a promoted post reads as astroturf, and it is the cheapest qualitative feedback any channel will ever give you.
What our own flight actually bought
The honest part. Our first Reddit flight — nine product-led ads, run through this same pipeline — bought 23,204 impressions and 184 clicks at a 0.79% click-through rate. Traffic metrics a lot of people would call healthy. Signups from it: none we could attribute.
The finding was not really about the creative. It was that we had waived our own "measurement before spend" rule to get the flight out, so the platform could never bid toward a signup and we could never join a click to one. Cheap clicks that buy nothing look exactly like cheap clicks that buy something, right up until you try to read the number that matters. We re-cut the creative toward content rather than product hooks, and then paused the whole thing during a repositioning.
So: the copy bar stopped us shipping something embarrassing. It did not make the channel work. Those two get confused constantly, and keeping them apart is most of what running a channel honestly consists of.
If you are doing this yourself
- Say which platform the budget is for. Absent is not zero, in our product and in most others.
- Pick the post format explicitly, and know what it costs you: an image post has no body, a text post has no button.
- Give the hero one job — survive a scrolling feed at 1.91:1 — and put every word in the title.
- Write the platform's copy rules down where the code reads them, not in a prompt you retype. A rule that is not in the pipeline is a rule you will forget on a Friday.
- Validate, preview, then push, and read the delivery status afterwards. A success response is a claim about your request, not about your campaign.
Run all four platforms from one place.
Build, optimize and report on Google, Microsoft, Reddit and Meta campaigns without an agency retainer.
See what it costsRead next
engineering
Why the Reddit Ads API was harder than it looks
We budgeted a day to integrate Reddit Ads and it took two weeks. Here are the refusals we hit, why our tests never caught them, and how it ends.

How to create beautiful user-centric products with AI
The models are the easy part. What keeps a product user-centric is the harness around them, from principles and stories to prototypes, tests and gates.