Documentation

Integrate yeld

One React component, or one HTTP call, puts clearly-labeled contextual ads into a free app — one unit on each of its primary screens. This page is the complete contract: props, placement requirements, how to make the unit look native, API responses, and what yeld does and does not collect.

Using a coding agent? Point it at /llms.txt instead of this page. It is the canonical machine-readable spec — install steps, the normative placement rules, the API contract, and the verification checklist — and it is what Claude Code and Cursor read.

Overview

yeld is an ad network for free software. Each unit requests one ad on page load, renders it natively as a sponsored card, and routes the click through yeld for attribution. On no fill it renders nothing at all.

Advertisers submit text and an optional logo — never markup, never scripts. yeld owns rendering end to end, which is why there is no third-party JavaScript in your bundle, no layout surprise, and nothing for you to sanitize.

Quick start

The fast path is one command — no account needed. The manual path is at the end; for non-React stacks see the script embed or the HTTP API.

01

Place

npx @yeld/cli init --apply

From your project root. It detects your stack (Next.js, Vite, Svelte, Vue, Astro, plain HTML), provisions an app ID on the spot, installs @yeld/react or the script embed, writes the env var, and inserts one unit on your primary screen — showing you the diff and asking first. It only edits at high placement confidence and never when a unit already exists, so re-running it is safe.

It prints a claim code. Save it: it is the only way to take ownership of the app and everything it earns, at /dashboard/claim. Already have an app ID from the dashboard? Pass it: npx @yeld/cli init app_… --apply.

02

Look

Run your app. The unit is already serving — your app starts in preview, where real matched creative (or a clearly-labeled sample when no campaign matches) renders immediately, and nothing bills or earns. Move the unit, restyle it, take your time: preview traffic is free for everyone.

03

Verify

Open a screen with a unit on it, and let the unit check itself:

npx @yeld/cli verify --wait

It reports, per screen and per viewport, whether the unit rendered on load without a user action, is visible, is above the fold, is wide enough, is not buried in a footer, is alone in its viewport, and did not shift the layout — with the fix for anything that failed. No screenshots, no resizing a browser. Fix it, reload, run it again.

The self-check runs only from a development origin (localhost, a private LAN address, a .local/.test host) and reports viewport size, a bounding box, and a pathname — never anything about your users. Pass selfCheck={false} to turn it off. Nothing rendered on a {"fill": false} response is correct, and verify says so rather than failing you.

04

Launch

npx @yeld/cli launch

The deliberate end of the preview. It shows the verify state, asks, and flips the app live: real ads serve from the next request, clicks bill advertisers, and 70% of every click is credited to you. Until you claim the app, earnings are held as provisional — owed, but payable only after the claim.

Want units on more screens later? npx @yeld/cli add proposes the next placements, one per screen. Your app in the dashboard also has a Check installation button, which turns green as soon as any valid request has arrived, including a no-fill one.

05

By hand (optional)

Everything init does can be done manually. Create an app at /dashboard/apps/new (or keep the provisioned one), install the SDK, and point it at yeld:

npm i @yeld/react
.env.local
NEXT_PUBLIC_YELD_BASE_URL=https://yeld.dev

The env var is optional — https://yeld.dev is the default (Vite reads VITE_YELD_BASE_URL). Set http://localhost:3000 to develop against a local yeld server, or pass baseUrl as a prop. Then add the component:

components/app-shell.tsx
import { YeldAd } from "@yeld/react";

<YeldAd
  appId="app_k3j9x2m1p4q8"
  placement="sidebar"
/>

One unit, on your app's primary screen — and a sidebar slot is the best home for it, because a persistent rail is on the primary screen and travels to every other screen. That is the complete integration: the unit picks its own layout and matches your colors, radius, and theme at runtime. The placement rules are requirements, not suggestions.

<YeldAd /> props

appId is the only required prop. Every other prop has a working default.

PropTypeDescription
appIdstringRequired. Your public app ID, e.g. app_k3j9x2m1p4q8. Safe to commit — the client integration has no secret keys of any kind.
placementstringThe standard slot key: sidebar, post-action, in-content, or bottom-banner. Placements auto-register on the first request and are how per-slot performance is reported in your dashboard, so use accurate keys. See Placement rules.
context{ category?, event?, page?, keywords? }Contextual hints that influence matching only. Never personal data, never user-generated content. Pass one on every unit — see Optimize for conversion.
variant"card" | "compact" | "post-success"Presentation override. Omit it — the server picks the unit's layout per request from the container's measured width, the placement, and the creative's assets, and new formats ship without you touching code. Setting a variant pins the presentation and turns that off. post-success adds a short lead-in line for post-action slots.
theme"auto" | "light" | "dark"Defaults to auto: the unit samples its surroundings and matches the host's rendered theme — including class-toggled dark modes — falling back to prefers-color-scheme. Pin it to light or dark only if adoption gets it wrong.
adoptbooleanDefaults to true: at render the unit derives its colors from the nearest opaque ancestor background and its corner radii from the nearest rounded ancestor, and re-adopts when the host flips theme. Anything you set explicitly via style always wins. Set false to opt out entirely.
appearance"bordered" | "plain" | "inset"How much chrome the unit brings of its own. bordered(default) is its own card. plain is transparent and borderless, for when your container already supplies a card. inset is a subtle tinted block with no border. See Native integration.
classNamestringApplied to the container element, for sizing and spacing.
styleCSSPropertiesInline styles on the container element, and the place to override the unit's public CSS custom properties — --yeld-accent, --yeld-radius, and the rest of the CSS custom properties.
baseUrlstringServer origin override. Falls back to NEXT_PUBLIC_YELD_BASE_URL, then https://yeld.dev. Trailing slashes are stripped.

Placement rules

These are requirements, not suggestions. An integration that breaks them is not a valid integration, and apps that break them can be suspended. yeld runs in standard slots, the same way display advertising runs on the web.

Start with exactly one unit, on your app's primary screen — the screen a returning user actually works in, never an onboarding flow, a login page, or a marketing page. One well-placed unit is a complete integration; a unit buried at the bottom of a page nobody reads is not.

Adding more screens is your decision, made later: npx @yeld/cli add proposes the next placements — one per screen, in the same slot priority — whenever you want them.

The slots, in priority order

npx @yeld/cli plan picks the slot for you. If you are choosing by hand, work down this list and take the first slot the screen offers; if it has no natural slot, add an in-content container rather than inventing a novel placement.

  1. 01sidebar

    A card in a persistent sidebar or rail that is present across the app. The highest-value slot there is, because one unit here covers every screen.

    <YeldAd
      appId="app_k3j9x2m1p4q8"
      placement="sidebar"
    />
  2. 02post-action

    Directly beside or below the result the user just produced — a generated output, a finished export, a completed search. The highest-intent moment in the app. This is a coverage slot, the kind npx @yeld/cli add proposes on a second screen — the unit still renders on load, and the action is passed as context.event, never used as a trigger.

    <YeldAd
      appId="app_k3j9x2m1p4q8"
      placement="post-action"
      variant="post-success"
      context={{ event: "generation_complete" }}
    />
  3. 03in-content

    Inline in the main content column, after the first meaningful block of content — not at the end of the page.

    <YeldAd
      appId="app_k3j9x2m1p4q8"
      placement="in-content"
    />
  4. 04bottom-banner

    Full-width below the main content. A fallback only, for screens where none of the slots above exist.

    <YeldAd
      appId="app_k3j9x2m1p4q8"
      placement="bottom-banner"
    />

Prominence — hard requirements

  • At least one unit is visible without scrolling on the app's main screen, on both desktop and mobile.
  • Every unit renders on page load and sits in the same position on every visit.
  • Rendering is never gated behind a user action, an app event, a feature flag, or a non-default tab. Event-conditional placements are not a valid integration.
  • Units are reachable without signing in or paying, if the app has any such surface.
  • No unit is buried in a page footer, below the last content block, inside a collapsed accordion, or in a hidden tab. That is the weakest possible placement — never default to it.
  • Every unit fills the full width of its container, at a minimum of roughly 280px. Do not shrink it into a corner.
  • One unit per screen at a time. Never two in one viewport.
  • Never inside a modal, popup, or interstitial.
  • Never above or blocking the primary action.
  • No layout shift under the user.
  • Never disguised as app UI — the unit is labeled "Sponsored" by design, and that label is not yours to remove.

Screens that must never carry an ad

  • Authentication screens — sign-in, sign-up, password reset, OAuth callbacks.
  • Checkout and payment flows.
  • Error states.
  • Empty first-run screens, before the user has done anything.
  • Any surface displaying sensitive user data.

The unit renders nothing on its own when there is no matching inventory. Never build a fallback "house ad" that imitates it, and never fill the empty space with something that looks like a yeld unit.

Native integration

The unit should look like it was designed by whoever built the app. This is not decoration: a unit that reads as part of the product gets looked at, and a bolted-on banner does not.

Most of it is automatic. At render the unit samples the nearest opaque ancestor background to derive its colors — following class-toggled dark modes and re-adopting when your app flips theme — and the nearest rounded ancestor for its corner radii. The server picks the layout format per request from the container's measured width. Typography inherits from your font stack. The overrides below exist for the cases adoption cannot see; reach for them only when something is visibly wrong.

Wrap it in your own container

When the app already has a card or section component, use it, and mirror the section heading pattern around it. Then tell the unit to stop bringing chrome of its own:

<YeldAd
  appId="app_k3j9x2m1p4q8"
  placement="sidebar"
  appearance="plain"
/>
appearanceWhen to use it
borderedThe default. The unit draws its own bordered card. Use it when the unit stands alone in a layout gap with no host container around it.
plainTransparent, no border, no radius — the unit inherits the surface of whatever wraps it. Use it inside your own card or panel, so you do not end up with a border inside a border.
insetA subtle tinted block with no border. Use it for a distinct-but-native block inside a larger panel, the way a quiet callout reads in an app.

CSS custom properties

Every color, radius, and pad in the unit is a public custom property. Override them on the unit through style, or from any ancestor in your stylesheet — pointing them at your own variables is the shortest path to a native look.

<YeldAd
  appId="app_k3j9x2m1p4q8"
  placement="sidebar"
  appearance="plain"
  style={{
    "--yeld-accent": "#4f46e5",
    "--yeld-radius": "8px",
    "--yeld-border": "var(--app-border)",
  } as React.CSSProperties}
/>
PropertyControls
--yeld-fontFont family. Defaults to inherit, so typography comes from the host by default.
--yeld-bgUnit background.
--yeld-fgPrimary text color.
--yeld-mutedSecondary text: advertiser name, body line, labels.
--yeld-borderHairline border and divider color.
--yeld-accentCTA background.
--yeld-accent-fgCTA text color.
--yeld-radiusOuter corner radius.
--yeld-radius-smInner radius: logo tile and CTA button.
--yeld-pad-xHorizontal padding.
--yeld-pad-yVertical padding.

Typography inherits from the host by default — --yeld-font is inherit, so the unit picks up your font stack without you doing anything. Explicit style values always win over adopted ones, so an override sticks. Pin theme="light" or theme="dark" only if adoption reads your background wrong, and pass adopt={false} to turn the sampling off entirely.

Disclosure is not styleable. The "Sponsored" label and the "Ads by yeld" attribution stay visible in every appearance, variant, and theme. There is no prop that removes them and no override that hides them. Blending the styling is the goal; disguising an ad as app UI is never acceptable.

Optimize for conversion

Relevance is what makes a placement worth anything. yeld is paid on clicks and so are you, so the difference between a matched ad and a generic one is the difference between a slot that earns and a slot that does not.

Context metadata

Describe the request so yeld can match it. The shape is { category?, event?, page?, keywords? } — all optional, all short strings, keywords capped at ten entries. Pass one on every unit.

context — matching hints only
<YeldAd
  appId="app_k3j9x2m1p4q8"
  placement="in-content"
  context={{
    page: "editor",
    event: "export_finished",
    keywords: ["svg", "icons", "design"]
  }}
/>
  • Always pass page — which screen this is.
  • Pass event at action moments, describing what the user just finished.
  • Pass two to five keywords describing what the user is doing on that screen.

Context broadens matching; it never narrows it. Your app's registered category always qualifies on its own, so a wrong or missing context reduces relevance but never suppresses fill.

Two hard limits. Context influences matching only — it must never control whether the unit renders, so an event value is a hint about the page, not a trigger for showing an ad. And it must never contain personal data, user-generated content, email addresses, file names, prompt text, or anything else your user typed.

Format and spacing

  • Leave variant unset: the server chooses the unit's format per request from the container's measured width, the placement, and the creative's assets, so the right treatment shows up on its own.
  • The one variant worth setting is "post-success" in a post-action slot; it adds a short lead-in line above the creative.
  • Give the unit the same spacing as your other blocks, and do not crowd it against the app's own calls to action.

HTTP API

For non-React integrations. The app ID is public and there is no authentication on ad requests — the origin check does the gating.

Request an ad

POST /api/v1/ad
Content-Type: application/json

{
  "appId": "app_k3j9x2m1p4q8",
  "placement": "bottom-banner",
  "context": { "page": "home" },
  "sessionId": "anon-id",
  "containerWidth": 720
}

GET /api/v1/ad accepts the same fields as query parameters — appId, placement, sessionId, category, event, page, and a comma-separated keywords — which is convenient from a shell but identical in behavior. sessionId is optional and should be an anonymous, random, locally-stored value; it is used for duplicate and frequency detection only.

Responses

200 — fill
{
  "fill": true,
  "impressionId": "imp_ivf3k118724gfgph",
  "format": { "key": "banner", "layout": "horizontal",  },
  "ad": {
    "headline": "Know when your app goes down",
    "body": "Uptime checks every 30 seconds.",
    "cta": "Start monitoring",
    "imageUrl": null,
    "logoUrl": null,
    "advertiser": "Watchtower",
    "clickUrl": "https://yeld.dev/api/v1/click/imp_ivf3k118724gfgph"
  }
}
200 — no fill
{ "fill": false }

Treat no-fill as the normal case, not an error: render nothing and leave no gap. Responses are sent with Cache-Control: no-store.

format is the layout the unit should render, chosen server-side from the request's containerWidth, the placement, and the creative's assets: layout (vertical stacked card, horizontal single row, text one line), showImage / showLogo / showBody, cta (button or link), and density. Honor it — the catalogue evolves server-side, and integrations that render the descriptor pick up new formats with no code change.

ad fieldTypeNotes
headlinestringOne-line creative title.
bodystringSupporting line. Hidden by the "compact" variant.
ctastringCall-to-action label.
imageUrlstring | nullOptional creative image.
logoUrlstring | nullOptional advertiser logo. Falls back to an initial.
advertiserstringAdvertiser name. Must be displayed.
clickUrlstringyeld attribution URL for this impression. Link to it verbatim; never to the advertiser directly.

Status and error codes

StatusBodyMeaning
200{ "fill": false }Valid request, no matching inventory. Render nothing. This is a success, not an error — the request is still logged and still verifies your installation.
400invalid_requestThe body failed validation (missing or oversized appId, malformed context). The response includes field-level details.
404unknown_appNo app exists with that app ID. Check for a typo or a stale key.
403app_suspendedThe app has been suspended and will not be served ads.
403origin_not_allowedThe browser Origin does not match the host registered on the app.

Error bodies are { "error": "<code>" }; invalid_request adds a details object with per-field messages. The React SDK renders nothing on any non-200, so a misconfigured app ID degrades to empty space rather than a broken layout.

Clicks

Link the user to ad.clickUrl verbatim, in a new tab, with rel="noopener sponsored". yeld records attribution and redirects.

GET /api/v1/click/imp_…
302 Found
Location: https://advertiser.example/landing
Cache-Control: no-store

Attribution is recorded once per impression. Duplicate hits on the same impressionId still redirect, and are never counted or billed twice. A malformed impression ID returns 400; an unknown one returns 404.

Not an open redirect. The destination is read from the stored campaign or ad record and validated to be http or https. It never comes from the query string, so the endpoint cannot be pointed at an arbitrary URL by whoever holds the link.

CORS and origin rules

Server-to-server requests, which send no Origin, are always allowed, as are localhost and 127.0.0.1. A browser request from any other origin must match the host registered on the app; subdomains of that host are accepted. Test-mode apps skip the check entirely. Anything else gets 403 origin_not_allowed. The endpoint also answers OPTIONS preflights.

Custom integrations

If you render the unit yourself — Vue, Svelte, plain HTML, a native app, a CLI — you take on the disclosure requirements the React SDK handles for you. All of them are mandatory.

  • Show the advertiser name and a visible "Sponsored" label.
  • Show "Ads by yeld" attribution.
  • Render the creative text exactly as returned. Do not rewrite, truncate into nonsense, translate, or append to it.
  • Send the click through ad.clickUrl, opened with rel="noopener sponsored".
  • Style the unit to match the host app — that is encouraged — but keep the disclosure visible.
  • Follow the placement, native, and conversion rules identically: one unit on the primary screen, requested on page load, never event-gated, never in a footer.

Everything in Placement rules, Native integration, and Optimize for conversion applies identically to custom integrations: one unit on the primary screen, requested on page load, never event-gated, never in a footer. There is no separate, looser standard for hand-rolled units. Styling the unit to match your app is encouraged — you own the markup, so match your own type, radius, and accent — but the "Sponsored" label and the "Ads by yeld" attribution stay visible.

Test mode

A test-mode app behaves exactly like a live one — real matching, real creatives, real no-fill responses — but every row it produces is flagged and excluded from analytics, earnings, and campaign budget accounting. Nothing you do in test mode charges an advertiser or credits a publisher.

test-mode app ID
<YeldAd appId="app_test_sandbox" placement="sidebar" />

app_test_sandbox is the seeded sandbox app. Use it to exercise rendering, the no-fill path, and the network round-trip before your own app exists. Because test-mode apps skip the origin check, it also works from any local or preview host.

Any app can be flagged as test mode on its app record — ask yeld to flip yours while you are wiring things up. Excluded from all reporting: impressions, clicks, ledger entries, publisher earnings, advertiser spend, and campaign budget consumption. Swap in your real app ID before you ship.

Economics

Cost per click, and nothing else. Impressions are free: the advertiser is charged their campaign's CPC bid only when a valid click is recorded. Of that amount the publisher takes 70% and yeld takes 30%.

The ledger entry is written in the same database transaction as the click, so a recorded click and its money always agree. Unique constraints on the click and the ledger entry make double-billing impossible even under concurrent requests. Campaigns stop serving once total budget is spent, and once daily budget is spent for the day.

Publisher details covers payouts and reporting; advertiser details covers bids, budgets, and creative review.

Privacy

Contextual only. There is no behavioral profile to build, which is why there is nothing for your users to opt out of.

  • Matching is contextual: your app's registered category plus any context you pass. No user profile is involved.
  • No device or browser fingerprinting.
  • No third-party cookies.
  • No cross-site or cross-app behavioral profiles.
  • No reading of your users' content, your page contents, or the DOM around the unit.
  • No advertiser scripts or advertiser markup — yeld controls rendering, so nothing third-party enters your bundle.
  • One anonymous session ID in localStorage, used only for duplicate and frequency detection.

Full detail in the privacy policy. You remain responsible for whatever notices your own users require.