Authentication
Two key kinds reach this app, and the difference matters for one action only.
ads_live_… | Minted at /dashboard/api-keys. Reads, lists and exports. Cannot write a pack — it carries no ounie.com credential, so it can't reach your brains. |
ounie_live_… | Your ounie.com developer key, once you enable fleet access in settings. Everything, including brain-grounded writing. Draws the same wallet. |
Send it as Authorization: Bearer <key>, or as ?api_key=<key> when your client cannot set headers — the Ounie AI Team's manual MCP entry is the usual case. The dashboard's own session cookie works on the same routes.
POST /api/packs
Write one pack. Synchronous; a pack with four images takes about a minute.
{
"product": "A CRM built for solo real-estate agents", // required
"objective": "signups" | "sales" | "demo" | "install" | "awareness",
"brain_ids": ["<uuid>"], // 1–5, required
"placements": ["meta_feed","meta_story","google_rsa"], // 1–4, required
"angles": 3, // 1–6, default 3
"images": 2, // 0–4, default 0
"audience": "Agents closing 8–20 deals a year", // optional
"tone": "Direct, no hype" // optional
}Returns pack_id, the RSA pool, and the counts. Fetch the full pack with GET /api/packs/<id>, which signs every image URL fresh for 24 hours. That read is free, and so is re-signing.
What comes back per variant
{
"placement": "meta_feed",
"aspect": "1:1",
"headline": "…",
"primary_text": "…",
"description": "…",
"cta": "Start free",
"image_prompt": "…",
"image": { "url": "https://…", "width": 1024, "height": 1024,
"rendered_aspect": "1:1", "expires_in_seconds": 86400 }
}Errors
| 401 auth_required | No session and no valid key. |
| 403 master_key_required | An ads_live_ key tried to write a pack. Use your ounie_live_ master key, or the x402 rail. |
| 400 no_image_placements | Images were requested against a placement set with no image slot (google_rsa only). Refused before anything is billed. |
| 402 insufficient_credits | Carries required_credits, balance_credits and buy_credits_url. Nothing ran. |
| 422 thin_material | Your brains hold nothing usable about the product. Fully refunded. |
The placement contract
This is the part that makes the output usable rather than merely written. Each placement has its own frame, its own field set and its own character budget — and two different kinds of limit.
| id | aspect | delivered | headline |
|---|---|---|---|
| meta_feed | 1:1 | 1024×1024 | ≤ 40 |
| meta_story | 9:16 | 768×1376 | ≤ 40 |
| linkedin_single | 1.91:1 | 1376×720 | ≤ 70 |
| google_rsa | text only | no image | ≤ 30 (enforced) |
Google enforces its limits. A responsive search ad component over 30 characters is rejected on upload, so Ad Studio drops it rather than truncating it into half a sentence, and reports the drop in rsa.gaps. The pool is capped at 15 headlines and 4 descriptions, deduplicated case-insensitively; if fewer than 3 headlines or 2 descriptions survive, that is stated too, because below those counts Google will not run the ad at all.
Meta and LinkedIn truncate instead of rejecting, so their numbers are the point past which nobody reads the rest. Copy is written to the target and capped at the platform's cut, on a word boundary.
google_rsa never has an image, and never will — Google assembles text components at auction time. Requesting images against an RSA-only placement set is refused rather than silently billed.
LinkedIn's 1.91:1 is not in the image model's aspect enum, so that render comes back 16:9 and is centre-cropped to exactly 1.91:1 before it is stored. Every response carries rendered_aspect and the real pixel dimensions, so what you got is never a guess.
POST /api/x402/adpack · keyless
For an agent with a wallet and no Ounie account. It supplies the material itself as source_text (200–26000 characters) and citations point at that source rather than a brain page.
POST https://ads.ounie.com/api/x402/adpack
{
"source_text": "<your product brief, docs, positioning…>",
"product": "A CRM built for solo real-estate agents",
"objective": "signups",
"placements": ["meta_feed","google_rsa"],
"angles": 3,
"images": 1
}
→ 402 Payment Required
{
"x402Version": 1,
"accepts": [{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "240000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x…",
"extra": { "name": "USD Coin", "version": "2" }
}],
"quote": { "credits_equivalent": 20 }
}The price is computed from your payload — 8 credits for the copy pack plus 12 per image — never a constant, so the two rails cannot drift apart. Retry the same request with the signed X-Payment header.
What this rail refuses, and when
Anything knowable from the request alone and always fatal — a bad enum, source text under the minimum, images against a text-only placement set — returns 4xx with no quote in the body. Making you sign an authorization for a request that can never succeed would be the worst thing this endpoint could do.
Anything only knowable after the work — thin material, a model that returned nothing — is caught after your payment is verified but before it is settled, so nothing moves on chain and your signed authorization simply goes unused.
And because an on-chain settlement cannot be partially refunded, this rail is all-or-nothing on images: if a render fails twice, the endpoint returns 402 partial_delivery and does not settle. The credit rail, which can refund, settles on the images actually produced instead.
MCP
Point any MCP host at https://ads.ounie.com/api/mcp (Streamable HTTP; /api/sse for legacy clients). The Ounie AI Team cannot set static headers on a manual HTTP entry, so append ?api_key=… instead.
| tool | what it does | cost |
|---|---|---|
| list_ad_packs | Your generated packs, newest first. | free |
| get_ad_pack | One pack: angles, per-placement variants, the RSA pool, freshly signed 24h image URLs. | free |
| generate_ad_pack | Write a placement-sized pack grounded in your brain(s). Master key only. | 8 cr + 12 per image |
| export_ad_pack | CSV — one row per ad unit plus the RSA components. | free |
| get_placements | The placement table: aspect ratios, field sets, character limits, and which limits the platform enforces. | public |
| get_credit_balance | Spendable Ounie credits + monthly included. | free |
| get_pricing | Per-action pricing. No auth required. | public |
| whoami | The authenticated key's owner + key id. | free |
Grounding and honesty
Copy is written only from retrieved material. Every angle carries citations, and a citation to a page that was not retrieved is dropped before you see it — the model cannot invent a source. When no citation survives at all, the pack is flagged uncited rather than presented as grounded.
If your brains hold nothing usable about the product, the run is thin: it refunds in full and returns 422. Handing back invented features and made-up numbers would be worse than handing back nothing, because you are the one who has to defend the claim after you have spent money behind it.
Ad Studio writes creative. It does not audit anyone else's ads, and it does not claim to know what a competitor is currently running.