# 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.