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.
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
| Feature | Description |
|---|---|
| Hosting | Deploy containers to *.apps.buddocloud.com with one API call |
| Ads | Run campaigns or serve other operators' ads (earning tokens per impression) |
| Payments | Users spend tokens in your app; spent tokens credit your app balance |
| Analytics | DAU, retention, earnings, campaign performance |
| Social | Friends, presence, chat — free for all apps |
Token types — know which to use where
| Token | How to get it | Where it works |
|---|---|---|
Session JWTey... |
POST /api/auth/login |
Account management, OAuth app registration (/api/oauth/apps, /api/oauth/my-apps) |
OAuth access tokenat_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
}
}
Registration errors:
| Status | Error | Fix |
|---|---|---|
| 422 | email already taken | Log in instead |
| 422 | username already taken | Pick a different username |
| 422 | password too short | Use 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
}
}
client_id and client_secret
immediately. The client_secret is shown only once and cannot be recovered.
Field notes:
redirect_urismust be an array (even if you only have one URI). Passing a string will fail validation.allowed_scopesis an array of scope strings.- Start with the minimum scopes you need. Request additional scopes in Step 4.
Available scopes
| Scope | What it grants | Auto-approved? |
|---|---|---|
profile:read | Read user profile; required to serve ads | Yes |
points:read | Read a user's point balance | Yes |
points:spend | Debit user balance; credits your app account | No — admin review |
points:transfer | Transfer points between users | No — admin review |
points:award | Award points from treasury to a user | No — admin review |
app:balance:read | Read your app's accumulated balance | No — admin review |
deploy:manage | Create and manage container deployments | No — admin review |
campaigns:manage | Create, update, and cancel ad campaigns | No — 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.
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:
| Field | Required | Description |
|---|---|---|
name | Yes | Lowercase letters, digits, hyphens only. Max 63 chars. Must start and end with a letter or digit. Becomes the subdomain: <name>.apps.buddocloud.com. |
image | Yes | Docker image URI. Allowed registries: harbor.buddo.xyz:4443/, docker.io/library/, ghcr.io/. |
tier | Yes | free (self-service, auto-approved) or basic (admin-promoted). |
port | No | Port your container listens on. Default: 3000. Use 8080 for nginx-unprivileged. |
env | No | Key-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: pending → pulling → running.
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.
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:
| Field | Required | Description |
|---|---|---|
name | Yes | Display name for your campaign |
surface_type | Yes | One of: banner, video, interstitial, native, rewarded |
total_payment | Yes | Total Buddo points to spend. Must be a positive integer. Rake is deducted automatically. |
payout_rate | Yes | Points paid per impression to the serving app |
starts_at | No | ISO8601 datetime. Defaults to immediately. |
ends_at | No | ISO8601 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.
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:
| Field | Required | Notes |
|---|---|---|
campaign_id | Yes | UUID of the campaign returned by the serve endpoint |
event_type | Yes | impression or click. Use click for user interactions. |
surface_type | No | Defaults to banner if omitted |
8.3 Rate limits for ad endpoints
| Endpoint | Limit |
|---|---|
GET /api/external/ads/serve | 60 requests/min |
POST /api/external/ads/event | 30 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
- Your
app_accountbalance is your only source of funds for campaigns and hosting. - You cannot mint or create tokens — only earn them through user spending and ad impressions.
POST /api/external/points/spenddebits the user and credits your app.- Buddo tokens are issued on the Obyte network. They are a token on Obyte, not backed by it.
- Tokens do not convert to Bitcoin. There is no cashout path.
- Never call
/api/internal/*endpoints — those are admin-only and will return 403.
Troubleshooting
Common errors
| Status | Error | Cause | Fix |
|---|---|---|---|
| 401 | Invalid or missing OAuth token | Access token expired, revoked, or wrong type | Refresh token (Step 5.3) or re-authorize |
| 401 | Invalid or missing session token | Session JWT expired | Log in again (POST /api/auth/login) |
| 403 | insufficient_scope | App does not have the required scope | Request scope (Step 4) and wait for approval |
| 403 | forbidden | Not the owner of this resource | Check you are using the correct token |
| 422 | validation_failed | Invalid field values | Read the details object for field-specific errors |
| 429 | Rate limit exceeded | Too many requests | Backoff: wait 1s, 2s, 4s between retries |
Deployment stuck at pending
- Check container port: if your container listens on port 80 (plain nginx), it will not receive traffic. Switch to port 8080 and use
nginx:alpine-unprivileged. - Check image registry: only
harbor.buddo.xyz:4443/,docker.io/library/, andghcr.io/are allowed. - Check container logs:
GET /api/deploy/apps/DEPLOYMENT_ID/logs
OAuth flow blocked at authorize
- Check
"is_approved": false— your app is pending admin review for elevated scopes. - Check
redirect_uriexactly matches one of the URIs you registered (no trailing slash difference). - Ensure
redirect_uriswas passed as an array during registration.
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)