# yeld
> yeld is the ad network for free software. Publishers add one React
> component to their free app and earn money from tasteful, relevant,
> clearly-labeled native ads. This file tells AI coding agents everything
> needed to integrate yeld into a publisher's app.
## What you are integrating
- One React component: `` from `@yeld/react`.
- It requests a single ad from the yeld API, renders it natively, and
routes clicks through yeld attribution. If there is no inventory it
renders nothing. It never injects third-party markup or scripts.
- The app ID is public and safe to commit. There are no secret keys in the
client integration.
## Start here
Everything below only needs `npm install` and plain HTTPS requests — no
third-party CLI to execute if you'd rather do this by hand. An optional
faster path (`@yeld/cli`) is described at the end of this section.
### Step 1 — get an app ID (no account needed)
Find the site's public URL yourself (a NEXT_PUBLIC_SITE_URL/VITE_SITE_URL-
style env var, package.json `homepage`, a canonical or og:url tag, CNAME,
robots.txt, vercel.json, the README's live link). Use the production
domain, never localhost.
POST {baseUrl}/api/v1/apps/provision
Content-Type: application/json
{ "url": "https://theirsite.com", "name": "Their App",
"category": "developer-tools" }
→ 201 { "appId": "app_...", "claimCode": "yeld-7K4M-QP2X",
"claimUrl": "{baseUrl}/dashboard/claim?code=yeld-7K4M-QP2X",
"host": "theirsite.com", "reused": false, "status": "preview" }
Calling it again for the same host returns the same app ID and claim code, so
re-running is safe.
If you truly cannot determine the domain — a brand-new project with no
deploy target yet — **omit `url` entirely**:
{ "name": "Their App" }
That creates an unbound app which adopts the first production site that
serves it. Nothing to configure later: the moment it serves a real (non-dev)
origin, that becomes its registered site. Prefer passing the URL when you
can find one, because that path is idempotent per host.
Units render immediately with that app ID, in **preview**: the unit renders
(real matched creative when inventory exists, a clearly-labeled sample
otherwise) but nothing bills and nothing earns until it is launched — see
"Preview → launch" below. Once launched, earnings accrue as **provisional**:
recorded and owed, but not payable until the user claims the app.
YOU MUST SHOW THE USER THE CLAIM CODE AND CLAIM URL, and tell them plainly:
the app and everything it has earned becomes theirs when they claim it, and
the claim code is the only way to do that. Put it somewhere they will keep
it — do not bury it in a log line.
Errors: `409 already_claimed` means that domain already belongs to a yeld
account — ask the user to sign in and copy the app ID from /dashboard/apps.
`429 rate_limited` means create the app from the dashboard instead.
### Step 2 — install and place the unit
See "Install", "Add the component", and "Placement rules" below — an
ordinary npm dependency plus one hand-written `` line, guided by
the rules in this file. Not a React app? See "Not a React app? Use the
script embed" instead.
### Step 3 — check it, then launch on the user's word
See "Test that it works" and "Preview → launch" below: both are plain HTTP
GET/POST calls against the app you provisioned in Step 1 — no CLI required.
### Optional faster path — @yeld/cli
`@yeld/cli` is published on npm by the yeld account. Verify it with
`npm view @yeld/cli` (the npmjs.com website blocks non-browser requests; the
registry API at https://registry.npmjs.org/@yeld/cli does not) — source
(plain, unminified CommonJS):
https://github.com/julesredd/yeld/tree/main/packages/cli. It automates
Steps 1–3 above. Prefer not to run an unfamiliar binary? Everything in this
file works without it.
Start with a read-only pass instead of exploring the repository by hand —
it does that analysis for you, writes nothing, and makes no network call:
npx @yeld/cli plan
That returns the framework, the primary screen, and the exact file/line/slot
it would use. Once that looks right, run the real integration from the
project root:
npx @yeld/cli init --apply --yes --json
One command is the whole integration: it provisions an app (Step 1),
installs the SDK, writes the env var, and inserts the unit itself (Step 2).
It only edits at high placement confidence and never when a unit already
exists, so re-running it is safe. Review `apply.diff` in the result. Pass an
app ID as the first argument if the user has one.
If `apply.applied` is false the result says why (low confidence, unusual
layout, anchor drift); place the unit by hand instead, using
`plan.recommendation` (`.file`, `.anchor.line`, `.snippet`) and the rules
below. `alternatives` lists other viable files.
`npx @yeld/cli init --apply` without `--yes` shows the diff and asks before
writing (for humans at a terminal). `npx @yeld/cli verify --wait` replaces
Step 3's check; `npx @yeld/cli launch --yes` replaces Step 3's launch call
(never run it without the user's explicit go).
## Install
npm install @yeld/react
(Published on npm: https://www.npmjs.com/package/@yeld/react — a single
self-contained file with zero dependencies beyond React.)
## Configure
Set the yeld server origin (defaults to https://yeld.dev):
NEXT_PUBLIC_YELD_BASE_URL=https://yeld.dev
For local testing against a locally running yeld server:
NEXT_PUBLIC_YELD_BASE_URL=http://localhost:3000
You can also pass `baseUrl` as a prop instead of using the env var.
## Add the component
import { YeldAd } from "@yeld/react";
That is the complete integration. The unit decides how it looks on its own:
the server picks a layout format per request (from the measured container
width, the placement, and the creative's assets), and the unit samples its
surroundings at runtime to match the host's colors, corner radii, and theme —
including class-toggled dark modes. Do NOT set variant, theme, appearance, or
CSS variables unless something is visibly wrong.
Props:
- `appId` (required): the publisher's public app ID.
- `placement` (optional string): the standard slot key — "sidebar",
"post-action", "in-content", or "bottom-banner" (see Placement rules
below). Placements auto-register on first request and are how per-slot
performance is reported, so use accurate keys.
- `context` (optional): safe contextual hints — `{ category?, event?,
page?, keywords? }`. NEVER send personal data, user-generated content,
emails, or anything sensitive.
- `variant` (optional): "card", "compact", or "post-success". Omit it —
pinning a variant overrides the server's per-request format choice.
- `theme` (optional): "auto" (default: adopt the host's rendered background,
falling back to prefers-color-scheme), "light", or "dark".
- `adopt` (optional, default true): the runtime host adoption. Explicit
`style` values always win over adopted ones; set false to opt out.
- `appearance` (optional): "bordered" (default, own card), "plain"
(transparent, no border, no padding — sits flush inside a card the host
already supplies), or "inset" (subtle tinted block, no border).
- `className` / `style` (optional): sizing/spacing, and manual overrides of
the public CSS custom properties (--yeld-bg, --yeld-fg, --yeld-muted,
--yeld-border, --yeld-accent, --yeld-accent-fg, --yeld-radius,
--yeld-radius-sm, --yeld-font, --yeld-pad-x, --yeld-pad-y).
- `baseUrl` (optional): yeld server origin override.
## Placement rules (REQUIREMENTS, not suggestions)
Place exactly ONE unit, on the app's primary screen — the screen a returning
user actually works in, never an onboarding, login, or marketing page. That
is the whole first integration; do not add more units unless the user asks.
`npx @yeld/cli plan` already picked the screen, the file, and the slot.
The slot priority (what the planner uses, and what to use if choosing by
hand):
1. "sidebar" — a card in a persistent sidebar or rail. Best slot in almost
every app: it is on the primary screen AND travels to every other screen,
so one unit covers the whole app.
2. "in-content" — inline in the main content column, after the first
meaningful block of content. NOT at the end of the page.
3. "bottom-banner" — full-width below the main content. Fallback only, when
neither of the above exists.
When the user wants more coverage later, `npx @yeld/cli add` proposes the next
placements (one per screen — e.g. a "post-action" unit beside the result the
user just produced, with `context={{ event: "..." }}`). Adding screens is the
publisher's opt-in decision, never the default.
Prominence (hard requirements — `npx @yeld/cli verify` checks every one):
- The unit MUST be visible without scrolling on the primary screen, on both
desktop and mobile.
- It MUST render on page load, in the same position on every visit. Do NOT
gate it behind a click, an app event, a feature flag, or a non-default
tab. Event-conditional rendering is not a valid integration. (If a result
area only exists after an action, mount the unit in the persistent
container AROUND that area — the one that renders on load — never inside
the conditional block that renders the result.)
- It MUST be reachable without signing in or paying, if the app has any such
surface.
- NEVER bury it in a page footer, below the last content block, inside a
collapsed accordion, or in a hidden tab. That is the weakest possible
placement — do not default to it.
- Full width of its container, minimum ~280px. Do not shrink it into a
corner.
- One unit per screen at a time. Never stack two in one viewport.
- Never inside modals, popups, or interstitials. Never above or blocking the
primary action. No layout shift under the user.
## Looking native is automatic — do not hand-style the unit
There is no design-matching step. At runtime the unit samples the nearest
opaque ancestor background to derive its colors (which follows class-toggled
dark modes, re-adopting when the host flips theme) and the nearest rounded
ancestor for corner radii; the server picks the layout format per request
from the container's measured width and the creative's assets. Typography
inherits from the host.
Only intervene when something is visibly wrong, in this order: wrap the unit
in the app's own card component if one exists; set appearance="plain"/"inset"
when the host already supplies that card; and only then override individual
`--yeld-*` custom properties via `style` (explicit values always beat adopted
ones).
The "Sponsored" label and the "Ads by yeld" attribution are always rendered
and must remain visible. Blending the styling is the goal; disguising an ad
as app UI is never acceptable, and there is no prop that removes disclosure.
## Optimize for conversion
Relevance is what makes the placement worth anything, so pass context:
context={{ page: "editor", event: "export_finished",
keywords: ["svg", "icons", "design"] }}
- Always pass `page`. Pass `event` at action moments. Pass 2-5 `keywords`
describing what the user is doing on that screen.
- Context influences matching only. It can never control whether the unit
renders, and it must never contain personal data or user content.
- Give the unit the same spacing as the host's other blocks. Do not crowd it
against the app's own calls to action.
## Screens that must never carry an ad
Authentication screens, checkout or payment flows, error states, empty
first-run screens before the user has done anything, and any surface
showing sensitive user data. The component renders nothing on its own when
there is no matching inventory — never build a fallback "house ad" that
imitates it.
## Test that it works (required before finishing)
Do NOT verify the placement with screenshots or by resizing a browser. The
unit measures itself. Start the publisher's dev server, open a screen with a
unit on it, then run:
npx @yeld/cli verify --wait
That reports, per screen and per viewport, whether the unit rendered on load
without a user action, is visible, is above the fold, is at least 280px wide,
is not buried in a footer, is alone in its viewport, and did not shift the
layout. Every failure comes with the fix. Exit code 0 means verified.
If it fails, change the placement, reload the page, and run it again — a new
measurement supersedes the old one for that screen and viewport. Load the page
at a narrow width too, so a mobile measurement is on record.
Local development is expected and supported. Requests from localhost, a
private LAN address, or a `.local`/`.test` hostname are automatically
recorded as **test traffic**: they serve real ads so you can verify the
integration, but they never earn the publisher money, never spend an
advertiser's budget, and never appear in reporting. The response includes
`"test": true` so you can confirm it. Nothing needs configuring, and
nothing needs undoing before deploy.
Self-checks are part of that: the SDK posts its own geometry to
`{baseUrl}/api/v1/verify` from development origins and from nowhere else. It
carries no user data — viewport size, a bounding box, and a pathname. Pass
`selfCheck={false}` (or `data-self-check="off"`) to turn it off locally.
Rendering nothing on a `{"fill": false}` response is correct — `verify`
reports a no-fill as such rather than as a bad placement. (No-fill only
happens on live apps: in preview the server always fills, sending its own
sample creative when no campaign matches, so there is always something on
screen to react to.)
Before deploying, make sure the yeld origin is the production one
(`https://yeld.dev`), not a localhost URL left in a script tag or env var,
and that the app's registered URL matches the site you deploy to. `www` and
subdomains are treated as the same site.
Finally: the publisher's yeld dashboard app detail page has a "Check
installation" button which turns green once a request has been received —
test traffic counts for that.
## Preview → launch (the feedback loop is the point)
A provisioned app serves preview traffic only: real matched creative when
inventory exists, the labeled sample otherwise, `"test": true` on every
response, nothing billed, nothing earned. This exists so the publisher can
look at the unit in their running app before anything is real.
The agent's job in preview, in order:
1. Tell the user the unit is on screen and where.
2. Ask them to react: right spot? feels native? anything to change?
3. Apply feedback (move the unit, spacing, appearance) and re-verify after
every move. Iterate until they approve.
4. Only when they say go:
npx @yeld/cli launch --yes
or without the CLI: `POST {baseUrl}/api/v1/apps/{appId}/launch` (no body,
idempotent — an already-live app reports `alreadyLive` rather than
failing). From that moment real ads serve, clicks bill advertisers, and
the app earns 70% of every click.
NEVER launch without the user's explicit approval. After launch, show the
diff, the `verify` result, and the claim code one more time, and mention
`npx @yeld/cli add` for more screens.
## Not a React app? Use the script embed
For vanilla JS/TS, Vite, Rails, Django, Astro, Svelte, plain HTML — anything
without React — do NOT hand-roll the HTTP call. Use the embed:
Per-slot attributes (each overrides the script tag's value): data-app-id,
data-placement, data-variant (omit — pinning one overrides the server's
format choice), data-appearance, data-theme, data-adopt ("off" to skip the
runtime host adoption), data-page, data-event, data-keywords (comma
separated), data-base-url, data-self-check ("off" to skip the local
installation self-check).
With no `[data-yeld-slot]` containers present, the script renders one unit
where the script tag itself sits — fine for a single slot, but containers are
preferred because they let you place units precisely and run several.
Client-side routing is handled automatically: the embed hooks history
(pushState/replaceState/popstate) and fills any slots the new view mounted.
Each slot requests once. `window.yeld.refresh()` remains available for
navigation the history hook cannot see (e.g. hash-only routers).
The embed builds the unit with DOM APIs (never innerHTML), does the same
runtime host adoption and format rendering as the React SDK, and renders
nothing on a no-fill. All Placement and Conversion rules above apply
unchanged.
## API (for non-React integrations)
POST {baseUrl}/api/v1/ad
Content-Type: application/json
{ "appId": "app_...", "placement": "bottom-banner",
"context": { "page": "home" }, "sessionId": "anon-id",
"containerWidth": 720 }
`containerWidth` is the px width of the element the unit will render into.
Send it: the server uses it to choose the layout format.
Response 200 with fill:
{ "fill": true, "impressionId": "imp_...",
"format": { "key": "banner", "layout": "horizontal",
"showImage": false, "showLogo": true, "showBody": true,
"cta": "button", "density": "cozy" },
"ad": { "id": "ad_...", "headline": "...", "body": "...", "cta": "...",
"imageUrl": null, "logoUrl": null, "advertiser": "...",
"clickUrl": "{baseUrl}/api/v1/click/imp_..." } }
Response 200 without fill: `{ "fill": false }` — render nothing.
While the app is in preview, every response also carries `"preview": true`
(nothing bills), and a response with `"sample": true` is the yeld-authored
stand-in served when no campaign matches — label it "Preview" instead of
"Sponsored" in custom integrations.
`format` is the layout the unit should use: layout ("vertical" stacked card,
"horizontal" single row, "text" one line), which pieces to show, whether the
CTA is a button or a text link, and the density. Render what it says — the
catalogue changes server-side, and integrations that honor it pick up new
formats with no code change.
Clicks: send the user to `ad.clickUrl` (new tab, `rel="noopener sponsored"`).
yeld records attribution and 302-redirects to the advertiser.
Render requirements for custom integrations: show the advertiser name, a
visible "Sponsored" label, and "Ads by yeld" attribution. Do not modify
the creative text. Style it to match the host app — that is encouraged — but
disclosure stays visible. The Placement and Conversion rules above apply
identically: one unit on the primary screen, requested on page load, never
event-gated, never in a footer.
## Privacy
yeld is contextual only: no fingerprinting, no cross-site profiles, no
third-party cookies. `sessionId` is an anonymous random ID stored in
localStorage, used for frequency/duplicate detection only.