| name | chatgpt-image-ad |
| description | Generate one or more standalone Meta image-ad creatives via ChatGPT Image 2 (gpt-image-2) through the Novoads API. Locks the model, auto-strips platform chrome, enforces edge-safe layouts and glyph-safety inside body text. Use when the user asks for a "gpt-image-2 ad", "ChatGPT Image ad", "Image 2 ad creative", "make a static image ad with GPT", or anchors on a need for typography-heavy / dense-text / UI-mimicry ad creatives (chat threads, comparison tables, fake search results, iOS dialogs, Slack snapshots, ChatGPT-conversation ads, Apple Notes lists). Does NOT trigger on Nano Banana cues — use nano-banana-image-ad for those. |
chatgpt-image-ad
Generate one or more standalone Meta ad image creatives via Novoads' POST /v1/images
with model: "gpt-image-2". Hands the image paths off to your Meta-ad-builder skill — this
skill does not upload to Meta itself.
If the shared/ files are not on disk
Then this skill was installed on its own from skills.sh and the rest of the pack stayed behind.
Fetch each missing file from https://raw.githubusercontent.com/novoads/claude-code-ads/main/<path>,
where <path> is one of shared/skills/image-ad-prompting/OVERVIEW.md,
shared/skills/image-ad-prompting/prompting/prompt-library.md,
shared/skills/image-ad-prompting/prompting/safety-suffixes.md,
shared/skills/image-ad-prompting/prompting/template-format.md and
shared/skills/chatgpt-image-ad/prompting/guide.md. Or take everything at once with
git clone https://github.com/novoads/claude-code-ads.git. That install has no
scripts/check-novoads-env.sh either, so set NOVOADS_API_KEY in the environment yourself and
let the first call report the key.
Read order
Paths below are from the repo root. This skill is copied into .claude/skills/ and
.cursor/skills/ by sync-skill.sh, so a relative link out of it would break in those
copies — read these from the repo you are working in.
- This file — endpoint, auth, upload flow, workflow phases.
shared/skills/image-ad-prompting/OVERVIEW.md — the ecosystem hub: which skill, which model, what the family does and doesn't do.
shared/skills/chatgpt-image-ad/prompting/guide.md — model-specific prompting (what gpt-image-2 is good/bad at, when to switch to nano-banana-pro).
shared/skills/image-ad-prompting/prompting/prompt-library.md — 40 validated templates with per-model notes.
shared/skills/image-ad-prompting/prompting/safety-suffixes.md — the 3 always-on guards.
skills/chatgpt-image-ad/scripts/generate_image.py — the helper script (Python stdlib only).
For anything about the API itself that this file does not answer — error codes, rate limits,
concurrency, the upload contract — the novoads-api skill's reference.md is the authority.
Hard rules — never relax
- Model is
gpt-image-2. The script refuses any other value. If the user asks for Nano Banana, point them at nano-banana-image-ad.
- No platform/screenshot chrome in output.
NO_CHROME_SUFFIX is always on (override with --allow-chrome only when the ad's concept requires chrome — rare).
- Edge-safe + glyph-safety suffixes always on unless
--no-safe-zone is explicit. They fix real failures; don't remove silently.
- Max 4 reference images.
referenceAssetIds caps at 4 on gpt-image-2 (nano-banana-pro takes up to 14, reve-2.1 8 — spec 2.7.0). The script enforces it.
- Every brand mark that appears in the frame is passed as a reference asset. Not a tip — a rule. This model invents brand-shaped content on any surface the prompt leaves unspecified, and it states it as fact. Measured over a 34-template run on one real product (2026-08-04): the label was faithful in all 34 because a reference pinned it, while unpinned marks failed three ways — a billboard invented a back-of-can view carrying "OUR WATER IS SOURCED FROM THE AUSTRIAN ALPS…" with an icon row and a barcode, a prop thermos rendered a Hydro Flask logo, and a publication wordmark drifted. So: pin the product, pin the logo, pin any third-party mark the concept needs; and for product surfaces the ad does not show, say so — "front label only; do NOT render the back of the can, no invented label copy, no ingredient text, no sourcing statement, no barcode." One retry with that wording cured the fabricated claim completely. Unpinned means invented. Pass it as
--pin-block "<one-line product description>" rather than writing the clause by hand: the script wraps the shared library's standard 346-character guard around your description, counts it against this model's prompt cap (32,000 characters, deployed spec 2.16.0, verified 2026-08-08), and refuses pre-network naming the pin block's share if it does not fit. Hand-written pin blocks ran ~700 characters and used to be the single largest cause of a refused prompt, back when the cap was 4,000 on every model. At 32,000 a hand-written block no longer overflows anything, and the reason to pass --pin-block is that the standard guard is the wording that was measured to work.
- No Meta upload from this skill. Image generation only. Hand off via filesystem paths.
- Always show a live cost estimate before generating, and get an explicit yes. The price comes from in this session — never from memory, never from , never from . There are no credit numbers written down anywhere in this repo, on purpose.
Prerequisites
.env containing NOVOADS_API_KEY (novo_ + 64 hex, created at https://novoads.ai/dashboard/settings?tab=api). Verify with ./scripts/check-novoads-env.sh.
- Optional:
PRODUCT_ID in .env so generated assets are filed under a specific product. Omit it and the job lands in your default product. productId is organizational only — it does not influence what is generated.
- Reference images on local disk (PNG/JPG/JPEG/WEBP). The script handles the upload flow internally — you pass local paths.
REST key required. A Novoads MCP connector is not a substitute. If
NOVOADS_API_KEY is missing or still the placeholder, stop before any
generation work and tell the user: "Before continuing, create an API key at
https://novoads.ai/dashboard/settings?tab=api and paste it into .env."
That holds even when mcp__novoads__* tools are connected and authenticated in
the session. Never call mcp__novoads__* tools from this repo's workflows: they
are a different surface with different behavior, including the units they quote
costs in. Repo installs verify with ./scripts/check-novoads-env.sh; a solo
install checks NOVOADS_API_KEY in the environment.
Configuration
- Base URL:
https://api.novoads.ai (or NOVOADS_BASE_URL). That is the host only — callers append /v1/....
- Auth:
Authorization: Bearer $NOVOADS_API_KEY.
- Endpoint:
POST /v1/images — synchronous. The call blocks for the render (typically 60–90 seconds) and returns the finished images. There is no job to poll and no asset endpoint to wait on.
- Reference uploads:
POST /v1/uploads with {contentType, sizeBytes} returns {assetId, uploadUrl, method, headers, expiresInSeconds, maxBytes}. PUT the raw bytes to uploadUrl echoing the returned headers byte for byte — Content-Type and Content-Length are both part of the signature, so storage returns 403 if either differs. The resulting assetId is durable and reusable: upload a product shot once and pass the same id on every later call.
What this model takes
| Field | Value |
|---|
model | gpt-image-2 (also the API default) |
prompt | the image prompt — the always-on suffixes count against the model's cap |
aspectRatio | 1:1 4:5 2:3 9:16 16:9 21:9 — defaults to 1:1, so always set it |
referenceAssetIds | up to 4, order preserved, addressable positionally from the prompt |
numImages | 1–4, one call, charged per image |
productId | optional, organizational only |
3:2, 3:4, 4:3 and 5:4 are not on this model — they exist on nano-banana-pro and
reve-2.1. The request schema is strict, so sending one is a 400 before anything is charged.
No template in the shared library needs them.
N variants is one call, not N calls. Send numImages: 4 and four images come back in
images[]. Do not fan out four parallel requests — that burns four of your five concurrency
slots to get the same result at the same price.
There IS an edit mode, on this model only. sourceAssetId edits an existing image
instead of drawing a new one — "remove the logo on the bottle", "make the background a
kitchen counter". POST /v1/images since spec 2.10.0; nano-banana-pro and reve-2.1
do not publish the field, so an edit is a reason to stay on this skill rather than switch.
Two rules the API enforces with a 400, not a shrug:
aspectRatio cannot be sent with sourceAssetId. An edit's output tracks the
source's shape — the service measures the source and renders at the closest cell of this
model's grid — so an aspectRatio alongside it would reframe the thing you asked to
preserve. Closest, not exact: this grid has nothing between 1:1 and 16:9, so a 4:3
landscape source renders 1:1. Portrait is never rendered landscape, or the reverse.
- The source spends a reference slot. Source +
referenceAssetIds must total ≤ 4.
There is still no mask and no inpainting: you describe the change in words. If the field is
missing from GET /v1/openapi.json, this deployment has the arm switched off — say so
rather than retrying.
Inputs the user must provide
| Input | Notes |
|---|
| Seed prompt | The creative direction in their words. You will rewrite it (see Phase 3). |
| Aspect ratio | One of the six above. Reject anything else. |
| Reference image(s) | Optional but strongly recommended when the ad features a specific product. Up to 4. |
Variant count N | Default 1. Cap at 4. Each one is charged. |
Workflow
Phase 1: Preflight
.env exists with NOVOADS_API_KEY.
- Health check:
./scripts/check-novoads-env.sh prints OK (a 401 is a bad key; a 403 with details.reason: plan_required is a good key on an account without API access — different fixes).
Phase 2: Gather inputs
Collect: seed prompt, reference paths, variant count, aspect ratio.
Present choices per shared/skills/image-ad-prompting/OVERVIEW.md § Presenting choices to
the user, which is one decision at a time and never this four-item brief up front.
Phase 3: Prompt rewrite
Read shared/skills/image-ad-prompting/prompting/prompt-library.md. If the user's brief maps
onto a template, check the Model notes block — only proceed if gpt-image-2 is marked
clean or preferred. If nano-banana-pro is preferred, suggest switching skills before generating.
Fill the {placeholders} and show the user the rewritten prompt. Ask "Use this, edit it, or
start over?" Loop until approved.
For fresh prompts (no template match), follow the structure in
shared/skills/chatgpt-image-ad/prompting/guide.md § Phase 3b.
Phase 4: Cost confirmation (MANDATORY)
curl -sS -X POST "$NOVOADS_BASE_URL/v1/estimates" \
-H "Authorization: Bearer $NOVOADS_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"kind":"image","model":"gpt-image-2","prompt":"<final prompt>","numImages":<N>}'
Free, and the only legitimate source of a price. Show the user credits, and whether
sufficient is true against their balance. If it is false, say so and stop — the response
carries shortBy and topUpUrl. Wait for an explicit yes before Phase 5.
Three things to know about this call:
- It says nothing about an image prompt.
POST /v1/estimates does return an advisory warnings array of craft notes, but every rule in it is video craft: an image estimate carrying the words that trip the video rules came back with no warnings key at all (verified live 2026-08-04). So for this route a weak prompt prices, charges and renders exactly like a strong one, and the rewrite rules in Phase 3 are the only check there is — apply them before you price.
- The body is strict. Only
kind, prompt, model, numImages, language are accepted. Sending aspectRatio or referenceAssetIds is a 400 Unrecognized key — the estimate never sees them, and neither affects the price.
- Its length check is looser than this model's. The estimate's own request schema caps
prompt at 50,000 characters for every image model, while POST /v1/images caps this one at 32,000 (deployed spec 2.16.0, verified 2026-08-08). A prompt between those two numbers prices cleanly and is then refused by the generation, so a clean estimate is not proof the prompt is sendable. generate_image.py measures against 32,000 before either call, which is the check that decides. The estimate does not run moderation either, so a prompt it blessed can still come back 422 content_policy.
Estimate the final prompt, the one the script will send with the safety suffixes
appended, or the quote will be short by roughly 1,500 characters' worth of prompt.
Phase 5: Generate
./skills/chatgpt-image-ad/scripts/generate_image.py \
--prompt "<rewritten>" \
--aspect-ratio <ratio> \
--n <N> \
--image-ref <product.png> \
[--image-ref <style-board.png>] \
--out ./generated \
--env-file .env
--image-ref uploads the file on every invocation. For a batch that reuses one product
photo across many prompts, upload it once and pass --ref-asset-id <assetId> instead —
same reference, no repeat upload. The two are interchangeable and share the 4-reference cap.
Each line on stdout is one JSON image (variant, path, job_id, width, height,
prompt, aspect_ratio, model, credits_charged).
Log the call to logs/novoads-api.jsonl per logs/README.md — images are written once,
complete (sync endpoint, no jobId to poll back). Record creditsCharged from the response,
never an estimate. Observability only, never a pricing source.
Phase 5b: For an edit run
Same seven phases; four differences, all of them consequences of editing rather than drawing.
./skills/chatgpt-image-ad/scripts/generate_image.py \
--prompt "Remove the sticker on the bottle. Change nothing else." \
--source-image ./generated/T36-....png \
--out ./generated \
--env-file .env
- No
--aspect-ratio. The script refuses the pair locally, before spending, for the
same reason the API does. Use --source-asset-id instead of --source-image when the
image is already uploaded — re-uploading the same bytes mints a second asset.
- The prompt names the CHANGE and pins everything else. "Remove the sticker" leaves the
rest of the frame unspecified, and unspecified is what this model reinvents. Say what
must not move: "Change nothing else. Keep the label, the wordmark, the lighting and the
framing exactly as they are."
- The wordmark rule applies harder here, not less. You are handing the model a picture
that already contains brand marks — the failure mode is it re-renders them slightly
wrong. Pass the brand's own wordmark as
--image-ref alongside the source when the
edit touches anything near it, and read the result for drift.
- Phase 6 QA is unchanged and still mandatory. An edit is a fresh render, not a patch:
nothing carries over pixel-for-pixel, so the whole frame is in scope for QA, not only
the region you asked about. Compare against the source side by side.
Cost is a normal image: an edit is one image, charged once, priced by POST /v1/estimates
like any other. Confirm it in Phase 4 the same way.
Phase 6: Visual QA (MANDATORY)
For each image, read it and inspect for:
- Garbled small text (most common gpt-image-2 failure on dense body text)
- Wordmark drift (if a brand wordmark wasn't passed as
--image-ref)
- Wrong text count (e.g. 4 Slack messages instead of 3)
- iOS dialog / UI proportion drift
If defective: regenerate with a revised prompt explicitly correcting the issue (see
shared/skills/chatgpt-image-ad/prompting/guide.md § Retry mode). Cap at 2 retries.
Each retry is a fresh charge — there is no free re-roll — so no re-confirmation is needed
within the cap, but report the extra credits at the end of the run. After the cap, stop and
show the best attempt.
Phase 7: Confirm and hand off
Show all paths to the user. Ask "Use all / use these specific ones / regenerate / cancel."
Open the output folder (open on macOS, xdg-open on Linux) so the user sees the results
without hunting for them.
Selected images are ready for the meta-ad-builder skill. Print the paths so the user can
pipe them. Optionally write them to ./generated/run-<ts>.jsonl for downstream consumption.
Full-library run
One product photo, the whole template library, one folder of finished ads. This is the
batch shape of the seven phases above — not a replacement for them. Every phase still
applies per image; what follows is the choreography around them.
1. Inputs. One product photo — a real product, with its real brand on it. Upload it
once with POST /v1/uploads and reuse that assetId on every template
(--ref-asset-id <id>); an assetId is durable, and re-uploading the same bytes per
template just mints redundant assets. Also collect the offer and brand facts the templates
interpolate (product name, claim, price, CTA). Output goes to one flat folder — one
folder for the whole run, not a folder per template.
2. Estimate before anything renders. One POST /v1/estimates for the run's image count
(Phase 4's rules apply unchanged: the estimate is the only price source, and it is free).
Show the total against balance and get an explicit yes before the pilot. Decide a
hard credit ceiling for the run at the same time, and check it before every request,
retries included — if the next call would cross it, stop and report what is left rather
than finishing the set. Worst case is not the happy path: every template retried twice is
three times the quoted number. If you drive this from a script, have it read prior spend
back off its own ledger, so a second process inherits the budget instead of restarting at
zero.
3. Pilot five, then check them yourself. Run T12 (fake ChatGPT), T14 (fake Slack), T33
(typography hero), T34 (iMessage), T38 (stat hero + chart) — five templates that
exercise dense UI text, conversation layout, pure type, and chart rendering, which is where
this model fails if it is going to. At most 4 requests in flight (the org concurrency
cap is 5, it is org-wide, and anything else the account is running shares it). Then Phase 6
QA on all five. This is the agent's checkpoint, not the user's
(shared/skills/image-ad-prompting/OVERVIEW.md § Presenting choices to the user): when the
five read right, continue into the remainder in the same run. Stop and show the images only
when the problem is systematic (a brand wordmark drifting, the product rendered from the
wrong angle), because that flaw is worth catching before 30 more images carry it.
4. The remainder. When this run was entered through OVERVIEW's option 1, this
engine's set is one half of one plan: the templates the library routes to the other
generator ride the same total and render in the same run
(shared/skills/image-ad-prompting/OVERVIEW.md § Presenting choices to the user).
Every other template whose Model notes block marks gpt-image-2
clean or preferred. Count them from prompt-library.md at run time; do not carry a number
from a previous run or from another product's library — entries get added, and Model notes
get corrected by exactly the kind of run you are doing now. Templates marked acceptable,
or whose note leads with a caveat rather than "clean", are not in the set; name them in the
report as skipped rather than silently dropping them. Same 4-in-flight cap. On 429, branch
on details.reason: concurrency_limit clears only by waiting, so honor Retry-After
instead of retrying tighter.
5. One image per call. Omit numImages (or send 1). A library run wants one render
per template, and a multi-image response is the only copy of images 2–N — a lost
response loses them while the credits stay charged (see the novoads-api skill's
reference.md). Write every image to disk the moment it arrives, before starting the
next template.
6. Report. Per-template credits summed from each response's creditsCharged (never the
estimate), the defect list from QA, what retries cost, and which templates were skipped or
left unrun and why. The QA defects are the useful output: they are what corrects the Model
notes in prompt-library.md. File those as follow-ups — do not edit the library mid-run,
that is clone-image-ad's job.
Out of scope — fail clearly
- Meta upload — the
meta-ad-builder skill.
- Nano Banana image generation — use
nano-banana-image-ad.
- Masked / inpainted editing —
sourceAssetId edits from a prompt; there is no mask, no region selection, no img2img strength dial. Describe the change in words.
- Video, carousel, DCO ads — image only. Video lives in the
novoads-api skill.
- Ad copy writing — different skill.
- Editing the shared prompt library — use
clone-image-ad.
Common errors
| What you see | What it means |
|---|
400 invalid_input with details.issues | A malformed field. Most often an aspectRatio this model doesn't take, more than 4 referenceAssetIds, or a prompt over the model's cap. Nothing was charged. |
401 unauthorized | Missing, malformed or revoked key. Send the user to https://novoads.ai/dashboard/settings?tab=api. |
402 insufficient_credits | details carries required and available. Tell the user the gap; do not retry. |
403 forbidden | details.reason says which: plan_required, subscription_inactive, or API access off for the account. |
422 content_policy | Moderation blocked it. The estimate does not run moderation, so this can land on a prompt the estimate priced cleanly. Nothing was charged. Rewrite or stop. |
429 rate_limited | Branch on details.reason. key_limit / organization_limit / client_limit are paced by slowing down; concurrency_limit is not — only waiting frees a slot. Honor Retry-After. |
403 on the presigned PUT | The signed headers weren't echoed byte for byte. Send the headers object from POST /v1/uploads exactly as returned. |
500 internal_error | Do not blindly retry — there are no idempotency keys, so a retry can double-charge. Check GET /v1/generations for a job that already landed. |
502 provider_failed | The model provider failed. Credits are refunded automatically. |
Files this skill owns
skills/chatgpt-image-ad/SKILL.md — this file
skills/chatgpt-image-ad/scripts/generate_image.py — the gpt-image-2 caller (upload + generate + download)
See also
shared/skills/image-ad-prompting/OVERVIEW.md — ecosystem hub, read first
shared/skills/chatgpt-image-ad/prompting/guide.md — gpt-image-2 strengths/limits, retry playbook
shared/skills/image-ad-prompting/prompting/prompt-library.md — 40 validated templates
shared/skills/image-ad-prompting/prompting/template-format.md — entry skeleton for new templates
shared/skills/image-ad-prompting/prompting/safety-suffixes.md — the 3 always-on guards
- the
clone-image-ad skill — reverse-engineers an existing ad into a reusable library entry
- the
nano-banana-image-ad skill — sibling for photoreal / lifestyle / multi-ref templates
- the
novoads-api skill — the API contract underneath all of this