# 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. ## Prerequisites — no account needed If the user gave you an app ID (looks like `app_k3j9x2m1p4q8`), use it. If they did NOT, do not stop: provision one yourself. The user does not need a yeld account for the integration to work end to end. FIRST, find the site's public URL yourself — you are in the repo, so do not ask the user for it unless you genuinely cannot determine it. Check, in order: - a deployed-URL env var: NEXT_PUBLIC_SITE_URL, NEXT_PUBLIC_APP_URL, VITE_SITE_URL, PUBLIC_URL, URL, SITE_URL (in .env*, or the host config) - `homepage` in package.json - a canonical or og:url meta tag in index.html or a layout/head file - CNAME, public/robots.txt, public/sitemap.xml, or a sitemap config - vercel.json / netlify.toml / wrangler.toml, or `vercel domains ls` - the README's live-demo link Use the production domain, never localhost. Derive the app name from package.json `name` or the site title. 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 } Calling it again for the same host returns the same app ID and claim code, so re-running the installer 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. Ads serve immediately with that app ID. 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. ## Install If the project is inside the yeld monorepo or has access to it: npm install @yeld/react (If the package is not available on the npm registry yet, the publisher can vendor `packages/react/src/index.tsx` from the yeld repository — it is 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"; 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" (default), "compact", or "post-success". - `theme` (optional): "auto" (default, follows prefers-color-scheme), "light", or "dark". - `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 the place to override the public CSS custom properties (see Native integration). - `baseUrl` (optional): yeld server origin override. ## Placement rules (REQUIREMENTS, not suggestions) Place a unit on EVERY primary screen of the app — one per screen, never more. A typical app ends up with 2-4 units. A single unit at the bottom of one page is an incomplete integration. First map the app: list the screens/views users actually spend time in, and note any persistent chrome (sidebar, rail, dashboard shell). Then use every slot below that applies, in this priority order: 1. "sidebar" — a card in a persistent sidebar or rail that is present across the app. Highest value, because one unit covers every screen. 2. "post-action" — directly beside or below the result the user just produced (generated output, finished export, completed search). Highest intent moment in the app. This is ADDITIVE — it does not replace a persistent slot, and the unit on that screen must still render on load. 3. "in-content" — inline in the main content column, after the first meaningful block of content. NOT at the end of the page. 4. "bottom-banner" — full-width below the main content. Fallback only, for screens where none of the above exist. If a screen has no natural slot, add an "in-content" container to it rather than inventing a novel placement. Prominence (hard requirements): - At least one unit MUST be visible without scrolling on the app's main screen, on both desktop and mobile. - Every unit MUST render on page load, in the same position on every visit. Do NOT gate a unit behind a click, an app event, a feature flag, or a non-default tab. Event-conditional rendering is not a valid integration. (A "post-action" slot lives on a screen the user reaches normally; the unit there still renders on load, and the event is passed as context. If the result area itself 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.) - Units MUST be reachable without signing in or paying, if the app has any such surface. - NEVER bury a unit 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. ## Native integration (do this — do not skip it) The unit should look like it was designed by whoever built the app. Read the host's design tokens (Tailwind config, CSS variables, an existing Card or Panel component) and match them: - Wrap the unit in the app's own card/section component when one exists, and mirror the surrounding section heading pattern. - Set appearance="plain" or "inset" when the host already supplies a card; use the default "bordered" when the unit stands alone. - Mirror the host's radius, accent, and borders through the public CSS custom properties: Available: --yeld-font, --yeld-bg, --yeld-fg, --yeld-muted, --yeld-border, --yeld-accent, --yeld-accent-fg, --yeld-radius, --yeld-radius-sm, --yeld-pad-x, --yeld-pad-y. Typography inherits from the host by default. - Set theme="light" or "dark" for a single-theme app; leave "auto" if the app follows the system preference. - The "Sponsored" label and the "Ads by yeld" attribution 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 on every unit: 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. - Use the default "card" variant in high-attention slots; reserve "compact" for dense persistent chrome. - 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) 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. Run the app as a new signed-out user and, for EACH screen you touched: 1. Confirm a POST to `{baseUrl}/api/v1/ad` fires on page load and returns 200 — without clicking anything. 2. Confirm the unit is visible in the viewport (or within one scroll) and looks like it belongs to the app. If seeing it requires ANY user action, the placement is wrong: move it and re-verify. 3. Confirm at least one unit is above the fold on the main screen, on desktop and mobile. 4. Confirm nothing shifts layout when the unit loads, and that no screen renders two units. Rendering nothing on a `{"fill": false}` response is correct. 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. ## 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, data-appearance, data-theme, data-page, data-event, data-keywords (comma separated), data-base-url. 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. For client-side routing, call `window.yeld.refresh()` after navigation to fill any newly added slots. Each slot requests once. The embed builds the unit with DOM APIs (never innerHTML), applies the same styling and CSS custom properties as the React SDK, and renders nothing on a no-fill. All Placement rules, Native integration, and Conversion sections 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" } Response 200 with fill: { "fill": true, "impressionId": "imp_...", "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. 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 rules, Native integration, and Conversion sections above apply identically: one unit per screen across every 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.