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

Five steps. Everything below assumes React; for anything else, skip to the HTTP API.

01

Create an app

Sign in and register your app at /dashboard/apps/new. You get a public app ID like app_k3j9x2m1p4q8. Register the app under the host it actually runs on — browser requests are checked against it.

02

Install the SDK

npm i @yeld/react

Or let the CLI do the install, write the env var, and print your exact next step:

npx yeld init

The package is a single self-contained file with no dependencies beyond React.

03

Point the SDK at yeld

.env.local
NEXT_PUBLIC_YELD_BASE_URL=https://yeld.dev

Optional — https://yeld.dev is the default. Set http://localhost:3000 to develop against a local yeld server, or pass baseUrl as a prop instead.

04

Add the component

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

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

A sidebar unit is the best first placement: a persistent rail is present on every screen, so one unit covers the whole app. Then keep going — place one unit per primary screen, using the highest-priority slot each screen offers. A typical app ends up with two to four. The placement rules are requirements, not suggestions.

05

Verify

Load each screen you touched as a signed-out, first-time user. Confirm a request to /api/v1/ad fires on load without you clicking anything, and that either the sponsored card is visible within one scroll or nothing rendered on a {"fill": false} response — both are correct. If seeing the ad requires any user action, the placement is wrong. At least one unit must be above the fold on the main screen, on desktop and mobile.

Then open your app in the dashboard and press Check installation. It turns green as soon as any valid request has arrived, including a no-fill one.

<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. Defaults to card, which is what you want in high-attention slots. compact is a single-row bar (body text hidden) for dense persistent chrome. post-success adds a short lead-in line for post-action slots.
theme"auto" | "light" | "dark"Defaults to auto, which follows prefers-color-scheme. Pin it to light or dark for a single-theme app.
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.

Place a unit on every primary screen of the app — one per screen, never more. A typical app ends up with two to four units. A single unit at the bottom of one page is an incomplete integration.

Map the app first

Before you place anything, list the screens and views users actually spend time in, and note any persistent chrome — a sidebar, a rail, a dashboard shell. That list is the plan: each screen on it gets one unit, and persistent chrome is the single highest-value place to put one.

The four slots, in priority order

Work down this list and use every slot that applies to a screen. If a screen has no natural slot, add an in-content container to it 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 slot is additive: it does not replace a persistent slot, and the unit still renders on load. 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. Never the whole integration.

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

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. Read your own design tokens — the Tailwind config, your CSS variables, an existing Card or Panel component — and make the unit match them. This is not decoration: a unit that reads as part of the product gets looked at, and a bolted-on banner does not.

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. Set theme="light" or theme="dark" if your app is single-theme; leave the default auto if it follows the system preference.

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.

Variant and spacing

  • Use the default "card" variant in high-attention slots — sidebar, post-action, in-content.
  • Reserve "compact" for dense persistent chrome, where a full card would not fit the rhythm of the app.
  • Use "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"
}

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",
  "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.

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 per screen across every 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 per screen across every 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.