Lulu Ads
Publisher-concierge MCP for the Lulu Ads monetization SDK — self-serve integration guide, publisher registration, and verification for any MCP server or agent tool. 70% CPA rev-share, disclosed sponsored data field, never a display instruction.
Documentation
lulu-ads
The monetization layer for the agent economy.
Monetize your MCP server or agent tool with one labeled sponsored line.
Quickstart · Integrations · Guarantees · API contract · Hosted docs · Blog · Become a publisher
70% to publishers · CPA only · 150ms fail-open · 0 prompt injections, by design
Lulu Ads attaches a disclosed, labeled data field to your tool's own result. The host model — Claude, Cursor, any agent — decides on its own judgment whether it's relevant enough to surface. We never instruct it to.
| What the SDK ships (a data field) | What the host renders (its choice) |
|---|---|
|
|
Zero-friction start — add the MCP server and let your agent do the rest:
claude mcp add --transport http lulu-ads https://ads.getlulu.dev/mcp
monetize my server
It'll fetch the right integration guide for your stack, register a publisher (with your consent), wire up the one-liner, and verify a slot went live.
If it renders and gets clicked, you earn 70% on CPA. If it doesn't — nobody pays, nothing breaks.
No prompt injection — we ship a data field; the host decides.
Quickstart
Python
pip install lulu-ads
# or: uv add lulu-ads
# or: poetry add lulu-ads
from lulu_ads import LuluAds
ads = LuluAds(publisher_id="pub_123", api_key="lk_...")
result = search_flights("TLV", "BKK", dates)
result["sponsored"] = await ads.sponsored_slot(
context={"tool": "search_flights", "category": "travel.flights"},
timeout_ms=150,
)
return result
FastMCP servers get it in one line — credentials come from the environment:
export LULU_ADS_PUBLISHER_ID=pub_123
export LULU_ADS_API_KEY=lk_...
mcp.add_middleware(LuluAdsMiddleware())
TypeScript
npm install lulu-ads
# or: pnpm add lulu-ads
# or: yarn add lulu-ads
# or: bun add lulu-ads
import { LuluAds } from "lulu-ads";
const ads = new LuluAds({ publisherId: "pub_123", apiKey: "lk_..." });
result.sponsored = await ads.sponsoredSlot({ context: { tool: "search_flights" } });
No publisher ID yet? See docs/quickstart.md — three
ways to get one, none of them gated on the others.
Framework integrations
| Stack | One-liner | Docs |
|---|---|---|
| FastMCP (Python) | mcp.add_middleware(LuluAdsMiddleware()) | → |
| LangChain / LangGraph (Python) | middleware=[LuluAdsAgentMiddleware()] | → |
| CrewAI (Python) | lulu_crewai.install() | → |
| MCP TS SDK | withLuluAds(server) | → |
| Runtime owners (chat bots, WhatsApp/Telegram agents) | model_output + format_suffix(sponsored) | → |
| Any other runtime / language | sponsored_slot(context) over the raw contract | → |
Widget rendering (MCP Apps UI)
The plain sponsored field always ships and always works — some hosts
render it as a card purely on the model's own judgment, no instruction
anywhere. For hosts that support the MCP Apps
extension (io.modelcontextprotocol/ui), you can additionally register an
actual rendered widget instead of relying on that judgment call:
from fastmcp import FastMCP
from lulu_ads.widget import register_sponsored_widget
mcp = FastMCP("my-server")
sponsored_app = register_sponsored_widget(
mcp,
endpoint_url="https://my-server.example.com/mcp", # your public MCP connector URL
text="Save 15% at checkout",
url="https://example.com/deal",
)
@mcp.tool(app=sponsored_app)
def search(...): ...
Same helper, official TS SDK, for MCP servers built in Node instead of Python:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerSponsoredWidget } from "lulu-ads/widget";
const server = new McpServer({ name: "my-server", version: "1.0.0" });
const appMeta = registerSponsoredWidget(server, {
endpointUrl: "https://my-server.example.com/mcp", // your public MCP connector URL
text: "Save 15% at checkout",
url: "https://example.com/deal",
});
server.registerTool("search", { ...appMeta }, handler);
Ships a floating, rounded, gradient card (same visual system as
getlulu.dev) with a disclosed Sponsored label —
still just markup, never a directive. Two host-specific quirks this
handles for you: Claude requires an undocumented _meta.ui.domain value
derived from your endpoint URL (self-computed here, not a credential), and
the widget must send a ui/notifications/initialized handshake on load or
Claude keeps the iframe hidden. Verified live against production
(dali.getlulu.dev/mcp, ext-apps#671),
current as of 2026-07-19 — Claude's own rendering of MCP Apps widgets was
broken platform-wide before that fix landed, so treat any "should render"
claim (including this one, elsewhere) as unverified until you've checked
it live in your own host.
Card content is fixed at registration time — a house-ad tier, same as the plain field's house-fill path, not re-rendered per call yet.
Guarantees (enforced in code, not just promised)
| Guarantee | How |
|---|---|
| A tool call can never break because of ads | every failure path returns None/null; hard 150ms wall-clock timeout |
| Always disclosed | label: "Sponsored" is set by the SDK, never sourced from the response body |
| No prompt injection, ever | we ship a data field; there is no display instruction anywhere in the contract |
| No PII leaves your server | context is filtered against an allowlist client-side, before any request is built |
| Quality-gated | every creative passes Dali scoring (≥70) before it can fill a slot |
| Intent, not identity | targeting uses this call's stated context only — no user profiles, no cross-session ID |
| Misconfigured? Still safe | missing credentials → client is inert, returns None/null, zero network calls |
Why not just…
…tell the model to mention a sponsor in its reply?
Display instructions get MCP servers delisted by registries that scan for
injected directives. We ship a plain data object — label, text, url —
with no field, anywhere in the contract, that tells a model how to render or
phrase anything.
…count impressions and charge per view? An "impression" only exists if a model actually rendered it, and that's unverifiable from the server side — easy to game, hard to audit. We charge CPA only, on a click that redeems a signed, server-verified token. Payment maps to a real user action, not a claim.
…scan the conversation to target better?
Reading transcripts to target ads is a privacy trap: everything a user says
becomes ad-targeting data. We accept six allowlisted context keys — tool,
category, query, route, locale, country — stated intent for this
call only. No transcripts, no profiles, no PII fields exist in the schema.
How it works
tool call
│
▼
your tool's own result
│
▼
POST /slot (150ms cap, allowlisted context only)
│
▼
labeled data field { label: "Sponsored", text, url } ← attached, never injected
│
▼
host / model judgment → renders it, or doesn't — not our call
│ user clicks
▼
GET /c/{token} → signed redirect, click recorded
│
▼
advertiser's affiliate rails → POST /postback on conversion
│
▼
70% publisher / 30% Lulu, on the ledger. Earnings accrue to your balance from the first audited conversion — cash out from $100.
Full wire-level detail: docs/contract.md.
Docs: https://getlulu.dev/docs · Quickstart · API contract · Integrations · Publisher signup · Quality gate: Dali · MIT
Changelog
- 0.2.0 —
register_sponsored_widget()(Python:lulu_ads.widget, now also TypeScript:lulu-ads/widget, official MCP SDK): registers a real rendered MCP Apps UI sponsored card on your server (not just the plain JSON field), handling Claude's undocumented iframe-domain requirement and theui/notifications/initializedhandshake for you. Generalizes the fix verified live ondali.getlulu.dev/mcpagainst ext-apps#671. Both SDKs produce byte-identical_meta.ui.domainvalues for the same endpoint URL. - 0.1.1 — persistent HTTP clients in the Python SDK (per-call client
construction could burn the entire slot budget on CPU-constrained
containers; clients are now created once per
LuluAdsinstance and reused with keep-alive). Fail-open behavior unchanged. - 0.1.0 — initial release: Python + TypeScript clients, FastMCP / LangChain / LangGraph / CrewAI / MCP-TS adapters, suffix helpers, MCP concierge onboarding.