API

Three on-ramps, one generator

An Ounie account calls REST or MCP with a bearer token and pays in credits. An agent with no account calls the x402 endpoint and pays USDC on Base. All three run the same prompt and the same validator, so the ad units are identical — only where the material comes from and how you pay differ.

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_requiredNo session and no valid key.
403 master_key_requiredAn ads_live_ key tried to write a pack. Use your ounie_live_ master key, or the x402 rail.
400 no_image_placementsImages were requested against a placement set with no image slot (google_rsa only). Refused before anything is billed.
402 insufficient_creditsCarries required_credits, balance_credits and buy_credits_url. Nothing ran.
422 thin_materialYour 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.

idaspectdeliveredheadline
meta_feed1:11024×102440
meta_story9:16768×137640
linkedin_single1.91:11376×72070
google_rsatext onlyno image30 (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 (20026000 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 payload8 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.

toolwhat it doescost
list_ad_packsYour generated packs, newest first.free
get_ad_packOne pack: angles, per-placement variants, the RSA pool, freshly signed 24h image URLs.free
generate_ad_packWrite a placement-sized pack grounded in your brain(s). Master key only.8 cr + 12 per image
export_ad_packCSV — one row per ad unit plus the RSA components.free
get_placementsThe placement table: aspect ratios, field sets, character limits, and which limits the platform enforces.public
get_credit_balanceSpendable Ounie credits + monthly included.free
get_pricingPer-action pricing. No auth required.public
whoamiThe 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.