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.

Treasury-funded payouts. The Buddo treasury pays both the operator and the viewing user. Operators do not transfer their own points to users for watching ads — the treasury handles both sides.

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 TypeDescriptionTypical 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
Rewarded ads are the foundation of the “watch ads for rewards” UX pattern. The user explicitly chooses to watch, the operator logs the event, and the treasury pays the user directly.

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)
StateDescriptionResumable?
draftCampaign created but not yet activeYes — activate via update
activeCampaign is live and serving impressionsN/A
pausedTemporarily halted; no impressions servedYes — resume via update
exhaustedBudget fully spent; terminal stateNo — create a new campaign
cancelledManually cancelled; unspent budget refundedNo
Budget enforcement. When you create a campaign you pass 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.
Daily impression cap. Each campaign is capped at 10,000 impressions per day by default. Once the daily cap is reached, 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

ParameterRequiredDescription
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 validation. The 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

EndpointLimitScope
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
  }
}
FieldTypeDescription
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

StatusMeaning
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

Auto-impression tracking (since v42.0). Impressions are recorded automatically at serve time — you do not need to POST a separate impression event. The treasury payout for the impression is triggered at serve. You only need to call 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)

FieldRequiredDescription
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"
  }
}
FieldTypeDescription
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

StatusMeaning
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:

Operators never pay users. The treasury pays both sides. An operator's earned-point balance (visible via GET /api/external/app/balance) reflects income from hosting ads — it is never debited to pay users for ad views.

Payout Flow

  1. Business purchases ad placements → funds enter the treasury (10% rake deducted at creation, non-refundable)
  2. Ad is served in an operator's app via GET /api/external/ads/serve
  3. Impression is recorded automatically at serve time (since v42.0) → no separate event call needed
  4. Treasury pays the operator their hosting share immediately
  5. Treasury pays the user their viewing reward immediately
  6. For clicks: operator app calls POST /api/external/ads/event with event_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:

  1. Show the offer. Display a button like “Watch Ad for 100 Points” in your app UI.
  2. Request a campaign. When the user taps the button, call GET /api/external/ads/serve?surface_type=rewarded to get an active campaign. The impression is recorded automatically at this point and the treasury payout is triggered — no separate event call needed.
  3. Display the ad. Render the campaign content (video, interstitial, etc.) in your app. Wait for the user to complete viewing.
  4. 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.
  5. Report clicks separately. If the user taps/clicks the ad, call POST /api/external/ads/event with event_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));
Handle no-campaign responses gracefully. When no campaign is available, the serve endpoint returns 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.

OperationScopeEndpoint
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