Operator Onboarding Guide

A complete walkthrough from zero to a live app on the Buddo points economy. Each step has a copy-paste curl example with the exact request body fields, expected HTTP status, and an abbreviated response.

Base URL: https://api.buddo.xyz — No SDK required. No credit card required.

Introduction

What is Buddo?

Buddo is a points economy platform. Users earn and spend Buddo tokens across every app in the ecosystem. As an operator, you build an app that plugs into this shared economy — your users already have accounts and token balances before they ever visit your app.

What operators get

FeatureDescription
HostingDeploy containers to *.apps.buddocloud.com with one API call
AdsRun campaigns or serve other operators' ads (earning tokens per impression)
PaymentsUsers spend tokens in your app; spent tokens credit your app balance
AnalyticsDAU, retention, earnings, campaign performance
SocialFriends, presence, chat — free for all apps

Token types — know which to use where

TokenHow to get itWhere it works
Session JWT
ey...
POST /api/auth/login Account management, OAuth app registration (/api/oauth/apps, /api/oauth/my-apps)
OAuth access token
at_live_...
PKCE authorize + token exchange All /api/external/*, /api/operator/*, /api/deploy/* endpoints

Step 1: Register an Account

Register with email, password, and username. All three fields are required. The username is your public handle across the platform.

curl -X POST https://api.buddo.xyz/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "operator@yourcompany.com",
    "password": "your-secure-password",
    "username": "yourcompany"
  }'

Expected: 201 Created

{
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "operator@yourcompany.com",
    "username": "yourcompany",
    "tier": "buddo",
    "points": 0,
    "referral_code": "ABC123",
    "email_verified": false,
    "registration_number": 42
  }
}
Note: The register response does not include a session token. You must log in separately (Step 5) to obtain a JWT.

Registration errors:

StatusErrorFix
422email already takenLog in instead
422username already takenPick a different username
422password too shortUse at least 8 characters

Step 2: Verify Your Email

You must verify your email before you can register an OAuth app. Two paths exist depending on your environment.

Production: click the email link

Buddo sends a verification email to the address you registered. Click the link in that email. The link calls POST /api/auth/verify-email with your token.

Development: self-verify (no email required)

In development mode, use the self-verify endpoint. You must be logged in (session JWT required in the Authorization header).

# First, log in to get a session JWT
curl -X POST https://api.buddo.xyz/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "operator@yourcompany.com",
    "password": "your-secure-password"
  }'

# Then call self-verify with that JWT
curl -X POST https://api.buddo.xyz/api/auth/self-verify-email \
  -H "Authorization: Bearer <session-jwt>"

Expected: 200 OK

{ "message": "Email verified successfully" }

If the endpoint is unavailable (production with email disabled), contact admin@buddo.xyz with your username to request manual verification.

Step 3: Create an OAuth App

Register your OAuth app using your session JWT. This gives you a client_id and client_secret for the PKCE flow.

curl -X POST https://api.buddo.xyz/api/oauth/apps \
  -H "Authorization: Bearer <session-jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "app_name": "My First App",
    "app_description": "A demo app on the Buddo platform",
    "website_url": "https://myapp.example.com",
    "redirect_uris": ["https://myapp.example.com/oauth/callback"],
    "allowed_scopes": ["profile:read", "points:read"]
  }'

Expected: 201 Created

{
  "app": {
    "id": "a1b2c3d4-...",
    "app_name": "My First App",
    "client_id": "ae82729e-c708-448c-8361-cde45318a5be",
    "client_secret": "DvwkftcdOlfxTV1MetTfHb...",
    "redirect_uris": ["https://myapp.example.com/oauth/callback"],
    "allowed_scopes": ["profile:read", "points:read"],
    "is_approved": false
  }
}
CRITICAL: Save client_id and client_secret immediately. The client_secret is shown only once and cannot be recovered.

Field notes:

Available scopes

ScopeWhat it grantsAuto-approved?
profile:readRead user profile; required to serve adsYes
points:readRead a user's point balanceYes
points:spendDebit user balance; credits your app accountNo — admin review
points:transferTransfer points between usersNo — admin review
points:awardAward points from treasury to a userNo — admin review
app:balance:readRead your app's accumulated balanceNo — admin review
deploy:manageCreate and manage container deploymentsNo — admin review
campaigns:manageCreate, update, and cancel ad campaignsNo — admin review

Step 4: Scope Approval

Apps that request only profile:read and/or points:read are auto-approved — your OAuth flow will work immediately. Any other scope requires manual admin review before the OAuth flow can issue tokens.

Request additional scopes

curl -X POST https://api.buddo.xyz/api/oauth/my-apps/YOUR_APP_ID/scope-request \
  -H "Authorization: Bearer <session-jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "requested_scopes": ["points:spend", "app:balance:read", "deploy:manage", "campaigns:manage"]
  }'

Expected: 201 Created

{
  "id": "scope-request-uuid",
  "status": "pending",
  "requested_scopes": ["points:spend", "app:balance:read", "deploy:manage", "campaigns:manage"],
  "inserted_at": "2026-06-01T12:00:00Z"
}

Check approval status

There is no automated SLA for scope approval. Poll your app's status to check when approval has been granted:

curl https://api.buddo.xyz/api/oauth/my-apps \
  -H "Authorization: Bearer <session-jwt>"

Look for "is_approved": true in the response. Once approved, the full OAuth PKCE flow will succeed and your access token will carry the granted scopes.

Step 5: Authenticate (Tokens & Refresh)

5.1 Log in to get a session JWT

Your session JWT is used for account-management endpoints (/api/oauth/apps, /api/oauth/my-apps). It is not the token you use for the external/deploy/operator APIs.

curl -X POST https://api.buddo.xyz/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "operator@yourcompany.com",
    "password": "your-secure-password"
  }'

Expected: 200 OK

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "550e8400-...",
    "email": "operator@yourcompany.com",
    "username": "yourcompany",
    "tier": "buddo",
    "is_admin": false,
    "email_verified": true,
    "totp_enabled": false
  }
}

The token field is your session JWT. It expires; call this endpoint again to renew it.

5.2 PKCE flow — get an OAuth access token

OAuth access tokens (at_live_...) are what your app's backend uses to call /api/external/*, /api/deploy/*, and /api/operator/*.

A. Generate code verifier + challenge

CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '/+' '_-')
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '/+' '_-')

B. Redirect the user to authorize

https://api.buddo.xyz/api/oauth/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https://myapp.example.com/oauth/callback
  &response_type=code
  &scope=profile:read+points:read
  &code_challenge=YOUR_CODE_CHALLENGE
  &code_challenge_method=S256

C. Exchange the code for tokens

curl -X POST https://api.buddo.xyz/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "the-code-from-callback",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "redirect_uri": "https://myapp.example.com/oauth/callback",
    "code_verifier": "YOUR_CODE_VERIFIER"
  }'

Expected: 200 OK

{
  "access_token": "at_live_...",
  "refresh_token": "rt_live_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "player_id": "550e8400-e29b-41d4-a716-446655440000",
  "session_token": "st_live_...",
  "session_token_expires_at": "2026-06-01T13:00:00Z"
}

access_token — use as Bearer in the Authorization header. Expires in 1 hour.
refresh_token — single-use rotating token. Expires in 30 days. Consume it before it expires.
player_id — UUID of the user who authorized your app. Store this alongside the tokens.

5.3 Refresh an expired access token

Refresh tokens are single-use and rotating. Each refresh issues a new access_token and a new refresh_token. Discard the old refresh token immediately after calling this endpoint.

curl -X POST https://api.buddo.xyz/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "rt_live_...",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'

Expected: 200 OK — same shape as the token exchange above.

Step 6: Deploy an App

Deploy a container to *.apps.buddocloud.com with a single API call. Requires the deploy:manage scope and an OAuth access token.

Port 8080 required for nginx-unprivileged images.

The standard nginx Docker image listens on port 80, which requires root privileges. Buddo containers run as unprivileged users, so port 80 is not available.

If your app uses nginx, you must use the nginx:alpine-unprivileged image (or equivalent) and configure it to listen on port 8080. Set "port": 8080 in your deploy request. A container listening on port 80 will start but will never receive traffic, and the deployment will appear stuck at pending.

Quick fix for nginx:

FROM nginx:alpine-unprivileged
# nginx-unprivileged listens on 8080 by default
COPY ./dist /usr/share/nginx/html

6.1 Check available tiers

curl https://api.buddo.xyz/api/deploy/tiers \
  -H "Authorization: Bearer at_live_..."

Expected: 200 OK

{
  "tiers": [
    {
      "name": "free",
      "description": "Free tier — 512MB RAM, 0.5 CPU, max 3 containers. Self-service, auto-approved.",
      "limits": { "memory_mb": 512, "cpus": 0.5, "max_deployments": 3 },
      "auto_approve": true
    },
    {
      "name": "basic",
      "description": "Basic tier — 1GB RAM, 1 CPU. Admin-promoted apps only.",
      "limits": { "memory_mb": 1024, "cpus": 1.0, "max_deployments": null },
      "auto_approve": false
    }
  ]
}

6.2 Create a deployment

curl -X POST https://api.buddo.xyz/api/deploy/apps \
  -H "Authorization: Bearer at_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "myapp",
    "image": "ghcr.io/myorg/myapp:v1.0.0",
    "tier": "free",
    "port": 3000,
    "env": {
      "NODE_ENV": "production"
    }
  }'

Expected: 201 Created

{
  "id": "deployment-uuid",
  "name": "myapp",
  "status": "running",
  "url": "https://myapp.apps.buddocloud.com",
  "message": "Deployment created and running"
}

Deploy field reference:

FieldRequiredDescription
nameYesLowercase letters, digits, hyphens only. Max 63 chars. Must start and end with a letter or digit. Becomes the subdomain: <name>.apps.buddocloud.com.
imageYesDocker image URI. Allowed registries: harbor.buddo.xyz:4443/, docker.io/library/, ghcr.io/.
tierYesfree (self-service, auto-approved) or basic (admin-promoted).
portNoPort your container listens on. Default: 3000. Use 8080 for nginx-unprivileged.
envNoKey-value env vars. BUDDO_ prefix is reserved. Max 50 keys.

6.3 Check free tier eligibility

curl https://api.buddo.xyz/api/deploy/sandbox-status \
  -H "Authorization: Bearer at_live_..."

6.4 Poll deployment status

curl https://api.buddo.xyz/api/deploy/apps/DEPLOYMENT_ID/status \
  -H "Authorization: Bearer at_live_..."

Expected: 200 OK

{
  "id": "deployment-uuid",
  "status": "running",
  "url": "https://myapp.apps.buddocloud.com",
  "container_id": "abc123def456"
}

Poll until status is running or error. Lifecycle: pendingpullingrunning.

6.5 Manage your deployment

# View all deployments
curl https://api.buddo.xyz/api/deploy/apps \
  -H "Authorization: Bearer at_live_..."

# View logs
curl https://api.buddo.xyz/api/deploy/apps/DEPLOYMENT_ID/logs \
  -H "Authorization: Bearer at_live_..."

# Stop
curl -X POST https://api.buddo.xyz/api/deploy/apps/DEPLOYMENT_ID/stop \
  -H "Authorization: Bearer at_live_..."

# Restart
curl -X POST https://api.buddo.xyz/api/deploy/apps/DEPLOYMENT_ID/restart \
  -H "Authorization: Bearer at_live_..."

# Delete (must be stopped first)
curl -X DELETE https://api.buddo.xyz/api/deploy/apps/DEPLOYMENT_ID \
  -H "Authorization: Bearer at_live_..."

Step 7: Create an Ad Campaign

The Buddo ad marketplace lets you create campaigns that run across all apps in the ecosystem. Campaigns are funded from your app's app_account balance. Requires the campaigns:manage scope and an OAuth access token.

How billing works: You pass total_payment (the total points you want to spend). The platform takes a 10% non-refundable rake. Your campaign budget is 90% of total_payment. On cancellation, the unspent campaign budget (not the rake) is refunded.

7.1 Create a campaign

curl -X POST https://api.buddo.xyz/api/external/campaigns \
  -H "Authorization: Bearer at_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Launch",
    "surface_type": "banner",
    "total_payment": 50000,
    "payout_rate": 100
  }'

Expected: 201 Created

{
  "campaign": {
    "id": "campaign-uuid",
    "name": "Summer Launch",
    "surface_type": "banner",
    "budget": 45000,
    "spent": 0,
    "rake_amount": 5000,
    "total_payment_made": 50000,
    "payout_rate": 100,
    "status": "active",
    "inserted_at": "2026-06-01T12:00:00Z"
  }
}

Campaign field reference:

FieldRequiredDescription
nameYesDisplay name for your campaign
surface_typeYesOne of: banner, video, interstitial, native, rewarded
total_paymentYesTotal Buddo points to spend. Must be a positive integer. Rake is deducted automatically.
payout_rateYesPoints paid per impression to the serving app
starts_atNoISO8601 datetime. Defaults to immediately.
ends_atNoISO8601 datetime. No end date if omitted.

7.2 Campaign lifecycle

active --[pause]--> paused --[resume]--> active
active --[budget exhausted]--> exhausted  [terminal, not resumable]
active --[cancel]--> cancelled  [refunds unspent budget]

7.3 Manage campaigns

# List campaigns
curl https://api.buddo.xyz/api/external/campaigns \
  -H "Authorization: Bearer at_live_..."

# Pause a campaign
curl -X PUT https://api.buddo.xyz/api/external/campaigns/CAMPAIGN_ID \
  -H "Authorization: Bearer at_live_..." \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}'

# Resume a paused campaign
curl -X PUT https://api.buddo.xyz/api/external/campaigns/CAMPAIGN_ID \
  -H "Authorization: Bearer at_live_..." \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

# Increase budget (rake applies to additional payment)
curl -X PUT https://api.buddo.xyz/api/external/campaigns/CAMPAIGN_ID \
  -H "Authorization: Bearer at_live_..." \
  -H "Content-Type: application/json" \
  -d '{"additional_payment": 10000}'

# Cancel campaign (refunds unspent budget, rake is non-refundable)
curl -X DELETE https://api.buddo.xyz/api/external/campaigns/CAMPAIGN_ID \
  -H "Authorization: Bearer at_live_..."

Step 8: Serve Ads in Your App

When your app serves ads from the Buddo marketplace, you earn tokens per impression. The treasury pays both the serving app and the user automatically. Requires the profile:read scope.

8.1 Request an ad to serve

curl "https://api.buddo.xyz/api/external/ads/serve?surface_type=banner" \
  -H "Authorization: Bearer at_live_..."

Expected: 200 OK

{
  "campaign": {
    "id": "campaign-uuid",
    "name": "Summer Launch",
    "surface_type": "banner",
    "payout_rate": 100,
    "targeting_metadata": null
  }
}

surface_type is required. Valid values: banner, video, interstitial, native, rewarded.

Impressions are recorded automatically at serve time. You do not need to send a separate impression event. However, clicks must still be reported manually (see below).

8.2 Report a click event

curl -X POST https://api.buddo.xyz/api/external/ads/event \
  -H "Authorization: Bearer at_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_id": "campaign-uuid",
    "event_type": "click",
    "surface_type": "banner"
  }'

Expected: 201 Created

{
  "event": {
    "id": "event-uuid",
    "campaign_id": "campaign-uuid",
    "event_type": "click"
  }
}

Ad endpoint reference:

FieldRequiredNotes
campaign_idYesUUID of the campaign returned by the serve endpoint
event_typeYesimpression or click. Use click for user interactions.
surface_typeNoDefaults to banner if omitted

8.3 Rate limits for ad endpoints

EndpointLimit
GET /api/external/ads/serve60 requests/min
POST /api/external/ads/event30 requests/min

Implement exponential backoff on 429 responses: wait 1s, 2s, 4s before retrying.

Step 9: Check Analytics

9.1 Operator analytics (DAU, retention, earnings)

curl https://api.buddo.xyz/api/operator/analytics \
  -H "Authorization: Bearer at_live_..."

Expected: 200 OK

{
  "earnings": 125000,
  "dau": 47,
  "retention_7d": 0.62,
  "period": "last_30_days"
}

9.2 Campaign spend tracking

# All campaigns with spend summary
curl https://api.buddo.xyz/api/external/campaigns \
  -H "Authorization: Bearer at_live_..."

# Single campaign detail (includes spent vs budget)
curl https://api.buddo.xyz/api/external/campaigns/CAMPAIGN_ID \
  -H "Authorization: Bearer at_live_..."

Expected: 200 OK (single campaign)

{
  "campaign": {
    "id": "campaign-uuid",
    "name": "Summer Launch",
    "budget": 45000,
    "spent": 12500,
    "rake_amount": 5000,
    "payout_rate": 100,
    "status": "active"
  }
}

Step 10: Check Balance

Your app_account balance is the source of funds for ad campaigns and hosting. It grows when users spend tokens in your app. Requires the app:balance:read scope.

curl https://api.buddo.xyz/api/external/app/balance \
  -H "Authorization: Bearer at_live_..."

Expected: 200 OK

{
  "balance": 125000,
  "app_id": "your-app-uuid"
}

Treasury rules

Troubleshooting

Common errors

StatusErrorCauseFix
401Invalid or missing OAuth tokenAccess token expired, revoked, or wrong typeRefresh token (Step 5.3) or re-authorize
401Invalid or missing session tokenSession JWT expiredLog in again (POST /api/auth/login)
403insufficient_scopeApp does not have the required scopeRequest scope (Step 4) and wait for approval
403forbiddenNot the owner of this resourceCheck you are using the correct token
422validation_failedInvalid field valuesRead the details object for field-specific errors
429Rate limit exceededToo many requestsBackoff: wait 1s, 2s, 4s between retries

Deployment stuck at pending

OAuth flow blocked at authorize

Discovery endpoints

# Health check
curl https://api.buddo.xyz/health

# OAuth discovery
curl https://api.buddo.xyz/.well-known/oauth-protected-resource

# OpenAPI spec
curl https://api.buddo.xyz/.well-known/openapi.json

# Verify a token
curl -X POST https://api.buddo.xyz/api/oauth/token/verify \
  -H "Content-Type: application/json" \
  -d '{"token": "at_live_..."}'

Quick Reference: The Complete Flow

Step  Endpoint                                        Auth required
----  ------------------------------------------------  ----------------
1.    POST /api/auth/register                           None
2.    POST /api/auth/self-verify-email                  Session JWT
      POST /api/auth/verify-email (prod: via link)      None
3.    POST /api/oauth/apps                              Session JWT
4.    POST /api/oauth/my-apps/:id/scope-request        Session JWT
      GET  /api/oauth/my-apps    (poll status)          Session JWT
5.    POST /api/auth/login                              None
      GET  /api/oauth/authorize  (PKCE)                 Session JWT
      POST /api/oauth/token      (exchange/refresh)     None
6.    GET  /api/deploy/tiers                            OAuth token
      POST /api/deploy/apps                             OAuth token (deploy:manage)
7.    POST /api/external/campaigns                      OAuth token (campaigns:manage)
8.    GET  /api/external/ads/serve?surface_type=banner  OAuth token (profile:read)
      POST /api/external/ads/event                      OAuth token (profile:read)
9.    GET  /api/operator/analytics                      OAuth token
      GET  /api/external/campaigns                      OAuth token (campaigns:manage)
10.   GET  /api/external/app/balance                    OAuth token (app:balance:read)