Ad Marketplace
Introduction
Buddo has a built-in ad marketplace that connects operators (apps that host ad placements) with businesses (advertisers that purchase those placements). When a user views an ad, the Buddo treasury pays both the operator and the user — creating a three-way value exchange where everyone benefits.
Operators register ad surfaces in their applications (banners, video slots, interstitials, etc.). Businesses create campaigns targeting those surfaces. The treasury funds all payouts, taking a 10% rake on ad purchases. Operators never pay users out of their own balance.
How It Works
The ad marketplace follows a six-step flow from placement registration through to payout:
Operator Business Buddo Platform
| | |
| 1. Register ad | |
| placements | |
| (surface types) | |
| | |
| | 2. Create ad campaign |
| | (name, surface, rate) |
| | |
| | 3. Purchase placements ──>|
| | (treasury takes 10%) |
| | |
| 4. Serve ads ──────────>| |
| GET /ads/serve | |
| in operator's app | |
| | |
| 5. User views ad | |
| POST /ads/event | |
| (impression/click) | |
| | |
| | 6. Treasury pays |
| <── operator payout ───|────────────────────────────|
| | user payout ──────────>|
| | |
Ad Surface Types
Buddo supports five standard ad surface types. Operators register which types their app supports, and businesses target campaigns at specific surface types.
| Surface Type | Description | Typical Use |
|---|---|---|
banner |
Static or animated image banner | Sidebar, header, or footer ads |
video |
Pre-roll or mid-roll video ad | Video content, streaming apps |
interstitial |
Full-screen overlay between content | Level transitions, page changes |
native |
In-feed ad matching app design | Content feeds, recommendation lists |
rewarded |
User opts in to watch in exchange for points | Games, apps with loyalty features |
Campaign Lifecycle
Campaigns move through a defined set of states. Not all transitions are reversible — understand the lifecycle before creating campaigns at scale.
draft ──[activate]──> active ──[pause]──> paused ──[resume]──> active
| |
|──[budget exhausted]──> exhausted |
| (terminal) |
|──[cancel]──> cancelled <───────────────┘
(refunds unspent budget)
| State | Description | Resumable? |
|---|---|---|
draft | Campaign created but not yet active | Yes — activate via update |
active | Campaign is live and serving impressions | N/A |
paused | Temporarily halted; no impressions served | Yes — resume via update |
exhausted | Budget fully spent; terminal state | No — create a new campaign |
cancelled | Manually cancelled; unspent budget refunded | No |
total_payment (the total points to spend). A
10% rake is deducted immediately and is non-refundable.
The remaining 90% becomes the campaign budget. If you cancel a campaign,
the unspent portion of the 90% budget is refunded to your app account —
the rake is never returned.
GET /api/external/ads/serve returns
200 OK with {"reason": "no_campaign_available"}
rather than a campaign object. Handle this the same way you handle a genuine
lack of active campaigns — hide or disable the ad placement until the
next day. A 429 response for daily cap exhaustion is planned
for a future API version.
Serving Ads
When your app needs to display an ad, request a campaign from the serve endpoint. The platform selects an active campaign matching the requested surface type.
Endpoint
GET
/api/external/ads/serve
Authentication
OAuth 2.0 Bearer token with the profile:read scope.
Query Parameters
| Parameter | Required | Description |
|---|---|---|
surface_type |
Yes |
One of: banner, video,
interstitial, native, rewarded
|
surface_id |
No |
UUID of a specific registered ad surface. If provided, must belong to
the requesting operator — passing another operator's
surface_id returns 403 Forbidden.
|
surface_id parameter
is validated against the authenticated operator. You cannot serve ads on a
surface registered by another operator. Attempting to do so returns
403 Forbidden.
Rate Limits
| Endpoint | Limit | Scope |
|---|---|---|
GET /api/external/ads/serve (v1) |
60 requests/min | Per operator |
GET /api/external/ads/serve/v2 |
120 requests/min | Per IP and per operator (both limits apply) |
cURL Example
curl "https://api.buddo.xyz/api/external/ads/serve?surface_type=rewarded" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Success Response (200)
{
"campaign": {
"id": "a1b2c3d4-...",
"name": "Summer Promo",
"surface_type": "rewarded",
"payout_rate": 50.0,
"bitcoin_tier": null,
"targeting_metadata": null
}
}
| Field | Type | Description |
|---|---|---|
id |
UUID | Campaign identifier — use this when recording events |
name |
string | Campaign display name |
surface_type |
string | The surface type this campaign targets |
payout_rate |
number | Points paid per qualifying event (treasury-funded) |
bitcoin_tier |
string | null | If set, campaign targets users on a specific Bitcoin tier |
targeting_metadata |
object | null | Additional targeting criteria set by the advertiser |
Error Responses
| Status | Meaning |
|---|---|
| 400 | Missing or invalid surface_type |
| 401 | Missing or invalid OAuth token |
| 404 | No active campaign available for the requested surface type |
Recording Ad Events
POST /api/external/ads/event for click events.
After serving an ad, record click interactions to trigger click-based payouts.
Each event is recorded once per campaign per user —
duplicates are rejected with a 409.
Endpoint
POST
/api/external/ads/event
Authentication
OAuth 2.0 Bearer token with the profile:read scope.
Request Body (JSON)
| Field | Required | Description |
|---|---|---|
campaign_id |
Yes | UUID of the campaign (from the serve response) |
event_type |
Yes | impression or click |
surface_type |
No | Ad surface type (defaults to banner) |
cURL Example
curl -X POST https://api.buddo.xyz/api/external/ads/event \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"campaign_id": "a1b2c3d4-...",
"event_type": "impression",
"surface_type": "rewarded"
}'
Success Response (201)
{
"event": {
"id": "e5f6g7h8-...",
"campaign_id": "a1b2c3d4-...",
"event_type": "impression",
"payout_rate_snapshot": 50.0,
"surface_type": "rewarded",
"inserted_at": "2026-05-21T14:30:00Z"
}
}
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique event identifier |
campaign_id |
UUID | The campaign this event belongs to |
event_type |
string | impression or click |
payout_rate_snapshot |
number | The payout rate at the time of the event (frozen for auditability) |
surface_type |
string | The ad surface type |
inserted_at |
datetime | ISO 8601 timestamp of when the event was recorded |
Error Responses
| Status | Meaning |
|---|---|
| 400 | Bad request — missing or invalid fields |
| 401 | Missing or invalid OAuth token |
| 404 | Campaign not found |
| 409 | Duplicate event — already recorded for this campaign/user |
| 422 | Campaign exhausted or validation failed |
Payouts
The Buddo ad marketplace uses a treasury-funded payout model. When a business purchases ad placements, the funds go to the Buddo treasury (minus a 10% rake). When users view or click ads, the treasury distributes payouts to both parties:
- Operator earns for hosting the ad surface in their app
- User earns for viewing or interacting with the ad
GET /api/external/app/balance) reflects income from hosting
ads — it is never debited to pay users for ad views.
Payout Flow
- Business purchases ad placements → funds enter the treasury (10% rake deducted at creation, non-refundable)
- Ad is served in an operator's app via
GET /api/external/ads/serve - Impression is recorded automatically at serve time (since v42.0) → no separate event call needed
- Treasury pays the operator their hosting share immediately
- Treasury pays the user their viewing reward immediately
- For clicks: operator app calls
POST /api/external/ads/eventwithevent_type: "click"
The payout_rate on a campaign indicates the per-event payout amount.
The payout_rate_snapshot on each recorded event freezes the rate
at the time of the interaction for auditability.
Ad Reach Multiplier
Each deployment tier includes an
ad_reach_multiplier field. Higher-tier deployments get increased
ad distribution, meaning their placements are served more frequently. This
incentivizes operators to upgrade their hosting tier for greater ad revenue.
The “Watch Ads for Rewards” Pattern
Rewarded ads are the most common integration pattern. The user explicitly opts in to watch an ad in exchange for Buddo points. Here is how to implement it:
- Show the offer. Display a button like “Watch Ad for 100 Points” in your app UI.
-
Request a campaign. When the user taps the button, call
GET /api/external/ads/serve?surface_type=rewardedto get an active campaign. The impression is recorded automatically at this point and the treasury payout is triggered — no separate event call needed. - Display the ad. Render the campaign content (video, interstitial, etc.) in your app. Wait for the user to complete viewing.
-
Confirm the reward. The treasury has already paid the user
at serve time. Show a confirmation message. Verify the user's updated
balance with
GET /api/external/points. -
Report clicks separately. If the user taps/clicks the ad,
call
POST /api/external/ads/eventwithevent_type: "click"to record the click interaction.
Integration Example
This example shows a complete rewarded-ad flow in JavaScript. The game shows a “Watch Ad for Points” button, serves the ad, records the event, and confirms the user's payout.
const API_BASE = "https://api.buddo.xyz";
async function watchAdForReward(accessToken) {
// 1. Request a rewarded ad campaign.
// Impression is recorded automatically at serve time (since v42.0) —
// no separate POST /ads/event call needed for impressions.
const serveRes = await fetch(
`${API_BASE}/api/external/ads/serve?surface_type=rewarded`,
{ headers: { "Authorization": `Bearer ${accessToken}` } }
);
if (serveRes.status === 404) {
showMessage("No ads available right now. Try again later!");
return;
}
// Daily cap exhausted — serve returns 200 with no campaign
const serveData = await serveRes.json();
if (!serveRes.ok || serveData.reason === "no_campaign_available") {
showMessage("No ads available right now. Try again later!");
return;
}
const { campaign } = serveData;
// 2. Display the ad to the user.
// The treasury payout has already been triggered for the impression.
await showAdOverlay(campaign);
// 3. Confirm the reward — no extra API call needed for the impression.
showMessage(`You earned ${campaign.payout_rate} points!`);
// 4. Optionally refresh the displayed balance
const balanceRes = await fetch(`${API_BASE}/api/external/points`, {
headers: { "Authorization": `Bearer ${accessToken}` }
});
const { points } = await balanceRes.json();
updateBalanceDisplay(points);
// 5. If the user clicks/taps the ad, report it separately
campaign.element.addEventListener("click", async () => {
await fetch(`${API_BASE}/api/external/ads/event`, {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
campaign_id: campaign.id,
event_type: "click",
surface_type: "rewarded"
})
});
});
}
// Wire up the button
document.getElementById("watch-ad-btn")
.addEventListener("click", () => watchAdForReward(currentToken));
404. When the daily
impression cap (10k/day) is exhausted, it returns 200 OK with
{"reason": "no_campaign_available"}. In both cases, hide or
disable the ad placement rather than showing an error to the user.
Required OAuth Scopes
Ad serving and event recording both use the profile:read scope.
If your app also reads balances or spends points, you will need additional
scopes.
| Operation | Scope | Endpoint |
|---|---|---|
| Serve an ad | profile:read |
GET /api/external/ads/serve |
| Record an event | profile:read |
POST /api/external/ads/event |
| Check user balance | points:read |
GET /api/external/points |
| Check app balance | app:balance:read |
GET /api/external/app/balance |
See the OAuth PKCE Flow guide for how to request scopes during authorization.
Further Reading
- User API Reference — Ad serving and event recording endpoint schemas
- API Reference — Full endpoint documentation for all Buddo APIs
- OAuth PKCE Flow — Set up authentication for your operator app
- Operator API — Analytics and app management endpoints
- Deploy API — Deployment tiers and the
ad_reach_multiplier - JWT vs OAuth Tokens — When to use each authentication method