API reference

HTTP API for api.latrinebot.com. This reference is for client integrations (dashboard apps, scripts that manage your own projects). Use the Metrics API guide for read-only OBS overlays (X-Metrics-Key).

Prefer a typed client? The same surface is published as @latrinebot/sdk (TypeScript) and as a machine-readable OpenAPI 3.1 spec you can feed into Postman, Insomnia, or any OpenAPI code-generator. See the Developers guide for examples.

Overview

Base URLhttps://api.latrinebot.com
FormatJSON request and response bodies
CORSEnabled for https://latrinebot.com, https://studio.latrinebot.com, and preview *.pages.dev
Version0.4.0 (GET /api/v1)

Authentication types

TypeHeader / flowUsed for
Session (JWT) Authorization: Bearer <token> After wallet SIWS, Google OAuth, or X OAuth - see Authentication
Session (query) ?token=<jwt> on SSE only GET /api/projects/:id/stream (EventSource cannot send headers)
Metrics key X-Metrics-Key: lb_live_... Read-only /api/v1/projects/:id/metrics|events
None - /api/health, public widget and showcase routes

Client model (LIVE mainnet)

The public dashboard is LIVE-only: there is no test-mode switch for end users. All on-chain automation (claim creator fees, buyback, holder airdrops) runs on Solana mainnet.

Legacy SIM / DRY values may still appear on very old projects in the database; they are not exposed in the client UI and are not selectable via this API. Operator testing uses a separate admin surface, not documented here.

Token CA and ticker

Every project is tied to one Pump.fun token. The contract address (CA) and ticker are exposed in API responses so overlays and tools can label the token correctly.

FieldWhereDescription
mint Project root, status, Metrics API Solana base58 mint (CA). Required before running cycles.
settings.tokenSymbol Project settings, Metrics API settings Ticker symbol without $ (example: LATRINE). Filled automatically from DexScreener when the mint is saved. Empty string if unknown.
settings.mint Project settings Same CA as top-level mint (kept in sync).
settings.rewardAsset Project settings, Metrics API settings What eligible holders receive: { kind, mint } with kind in SAME | SOL | USDC | CUSTOM. mint is a base58 SPL mint only for CUSTOM (else null). Defaults to { "kind": "SAME", "mint": null }. See Configuration.
settings.lastRewardSymbol Project settings stats, Metrics API Ticker of the reward actually airdropped in the last cycle (e.g. SOL, USDC, or a custom symbol). Server-owned; null until first drop.
settings.holderRewardChoiceEnabled Project settings Holder perk on the token page. See Configuration.
settings.socialClaimEnabled Project settings X post boost on the token page. See Configuration.
settings.holdFund Project settings (dashboard PATCH); public read via holdFund on realm live Creator hold reserve transparency: { mode: "simple" | "goal" | "guaranteed", template, customLabel, goalSol, guaranteed? }. Dex Vault is activated via POST .../hold-fund/guaranteed/enable, not a direct PATCH field. Live payload exposes computed heldSol, purposeLine, showProgress, and Dex Vault guaranteed summary. See Hold fund transparency and Dex Vault API.

Display tip: in your UI use $ + settings.tokenSymbol when the symbol is non-empty; fall back to shortened mint if not.

Mint programs: preflight accepts both legacy SPL Token and Token-2022 mints (typical Pump.fun tokens).

Resolve ticker before creating a project

GET /api/projects/symbol?mint=CA

Requires session auth. Does not require an existing project.

curl -s "https://api.latrinebot.com/api/projects/symbol?mint=YOUR_MINT" \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN"

Response

{
  "ok": true,
  "mint": "So111...",
  "tokenSymbol": "LATRINE"
}

When you POST /api/projects or PATCH with a new mint, the server resolves tokenSymbol again. Clients cannot set the ticker manually (server-owned field).

Discovery

GET / or /api

HTML landing in a browser; JSON catalog with Accept: application/json.

GET /api/health

Liveness probe. No auth.

{ "ok": true, "service": "Latrine Bot", "site": "https://latrinebot.com", "ts": "..." }

GET /api/v1

API name and version.

{ "name": "Latrine Bot API", "version": "0.4.0", "docs": "https://latrinebot.com/docs" }

Authentication

The dashboard supports three sign-in methods. All of them end in the same session JWT used on protected API routes. You do not need a Solana wallet to use Google or X login, but you still need a Pump.fun dev wallet secret under Credentials to run the bot.

Sign-in methods

MethodDashboard buttonAccount id in APIauthProvider
Solana wallet (SIWS) Connect wallet Base58 Solana pubkey (32 bytes) solana
Google OAuth 2.0 Continue with Google google:<google-sub> google
X OAuth 2.0 (PKCE) Continue with X x:<x-user-id> x

Separate accounts: each method creates its own user row and project list. Signing in with Google does not merge with a wallet account or X account. Use the same method you used when you created a project.

Session JWT

Which OAuth buttons are shown

GET /api/auth/providers

No auth required. Use before rendering login buttons.

{ "ok": true, "google": true, "x": true }

google / x are true only when the server has the matching OAuth client id and secret configured. If false, hide that button (the start URL still redirects with auth_error if called).

A. Solana wallet (Sign-In With Solana)

For Phantom and other wallets that support signMessage.

  1. GET /api/auth/challenge?wallet=PUBKEY - returns nonce, message, domain, uri (nonce TTL 5 minutes).
  2. User signs message in the wallet (UTF-8).
  3. POST /api/auth/verify with wallet, signature (base58), message, nonce.
  4. Response: { ok: true, wallet, token } plus session cookie.

GET /api/auth/challenge?wallet=

Rate limit: 20/min per IP. domain / uri match SITE_URL (latrinebot.com).

POST /api/auth/verify

Rate limit: 30/min per IP. Invalid nonce or signature returns 401.

B and C. Google / X OAuth (browser redirect)

Both providers use the same finish step on the dashboard. Server-side flow:

  1. Browser opens GET /api/auth/google or GET /api/auth/x (302 redirect to provider).
  2. User approves at Google or X.
  3. Provider redirects to API callback: /api/auth/google/callback or /api/auth/x/callback.
  4. API creates or loads user, mints JWT, stores it under a one-time login_code in KV (TTL 120s).
  5. API redirects browser to https://latrinebot.com/app/?login_code=UUID (or auth_error=... on failure).
  6. Dashboard JavaScript calls POST /api/auth/complete-oauth with { "code": "login_code" }, receives token, saves it, calls /api/auth/me.

B. Google

StartGET /api/auth/google
CallbackGET /api/auth/google/callback
Scopesopenid email profile
Account idgoogle:<sub> from Google userinfo
LabelEmail or display name when available

Redirect URI registered in Google Cloud Console must be exactly https://api.latrinebot.com/api/auth/google/callback (or your API origin if using OAUTH_API_ORIGIN).

C. X (Twitter)

StartGET /api/auth/x
CallbackGET /api/auth/x/callback
Scopesusers.read tweet.read
PKCES256 code challenge (required by X OAuth 2.0)
Account idx:<numeric-user-id>
Label@username when available

Use a separate X developer app for dashboard login (X_LOGIN_CLIENT_ID, X_LOGIN_CLIENT_SECRET). This is not the same as server credentials used to post announcement tweets for projects.

Callback URL in the X developer portal: https://api.latrinebot.com/api/auth/x/callback. If login fails with a callback error, verify this URL and avoid a wrong X_LOGIN_REDIRECT_BASE override.

GET /api/auth/google

302 to Google. Rate limit: 20/min per IP.

GET /api/auth/x

302 to X authorize. Rate limit: 20/min per IP.

POST /api/auth/complete-oauth

JSON: { "code": "uuid-from-login_code-query-param" }. Returns { ok: true, token } and session cookie. One-time use.

curl -s -X POST "https://api.latrinebot.com/api/auth/complete-oauth" \
  -H "Content-Type: application/json" \
  -d '{"code":"YOUR_LOGIN_CODE"}'

Current session

GET /api/auth/me

Requires Authorization: Bearer or session cookie.

{
  "ok": true,
  "wallet": "google:1162...",
  "authProvider": "google",
  "label": "you@example.com"
}

authProvider is solana, google, or x. label is a display string for the UI.

POST /api/auth/logout

Clears session cookie. No body required.

OAuth redirect errors

On failure the API redirects to /app/?auth_error=... instead of login_code. The dashboard shows the decoded message and clears the query string.

Common values: sign-in cancelled, expired state, invalid callback, provider not configured.

Projects

Max 25 projects per account. Mint (CA) is required on create and cannot be cleared later.

MethodPathAuthDescription
GET/api/projectsSessionList your projects
POST/api/projectsSessionCreate project (name, mint required; default mode LIVE)
GET/api/projects/symbol?mint=SessionResolve tokenSymbol for a CA
GET/api/projects/:idSessionGet one project
PATCH/api/projects/:idSessionUpdate name, mint, settings (not mode)
DELETE/api/projects/:idSessionDelete project, stop scheduler, archive credentials
POST/api/projects/:id/tiers/reset-defaultSessionReset eligibilityTiers to platform default table

Project object (public fields)

{
  "id": "uuid",
  "name": "My token",
  "mint": "base58-ca",
  "mode": "LIVE",
  "running": false,
  "hasSecrets": true,
  "settings": {
    "mint": "base58-ca",
    "tokenSymbol": "LATRINE",
    "cycleIntervalSec": 60,
    "minHolderBalance": 500000,
    "maxHolderBalance": 20000000,
    "minClaimSol": 0.005,
    "minReserveSol": 0.05,
    "rewardAsset": { "kind": "SAME", "mint": null },
    "holderRewardChoiceEnabled": false,
    "socialClaimEnabled": false,
    "holdFund": { "mode": "simple", "template": null, "customLabel": "", "goalSol": 0 },
    "eligibilityTiers": [
      { "mcUsd": 0, "minTokens": 500000, "holdCycles": 5 },
      { "mcUsd": 50000, "minTokens": 450000, "holdCycles": 6 }
    ]
  },
  "createdAt": "...",
  "updatedAt": "..."
}

settings omits operator-only fields (including legacy simulation tuning). Metrics API responses use the same public subset. Full settings keys are documented in Configuration.

Create project

POST /api/projects
{ "name": "My Project", "mint": "YOUR_CA_BASE58" }

Update project

PATCH /api/projects/:id
{
  "settings": { "cycleIntervalSec": 90 }
}

Changing mint re-fetches tokenSymbol. mint cannot be set to empty.

Public policy audit (mandatory): when settings changes fee split (buybackPercent, burnPercent), rewardAsset, holderRewardChoiceEnabled, socialClaimEnabled, or holdFund, the API appends POLICY rows to project_events and busts the token-page live cache. There is no request flag to skip logging. Severity for major holder cuts (drop ≥20 pp or to 0%) is server-defined. See Distribution transparency.

Update eligibility tiers only

Rows are normalized (sorted, deduped, holdCycles coerced). Legacy holdCyclesRequired is stripped.

PATCH /api/projects/:id
{
  "settings": {
    "eligibilityTiers": [
      { "mcUsd": 0, "minTokens": 500000, "holdCycles": 5 },
      { "mcUsd": 250000, "minTokens": 300000, "holdCycles": 10 }
    ]
  }
}

Credentials

Dev wallet secret (Base58 64-byte keypair) and RPC URL are encrypted at rest. Full secrets are never returned after save. The API derives and exposes the dev wallet public key in credentials metadata.

MethodPathDescription
POST/api/projects/:id/secretsSave or update devWalletSecret and/or rpcUrl
GET/api/projects/:id/credentialsMetadata only (pubkey, RPC host, provider)
POST/api/projects/:id/credentials/testPing RPC; returns ping and credentials

Save credentials

POST /api/projects/:id/secrets
{
  "devWalletSecret": "base58-secret-optional-if-updating-rpc-only",
  "rpcUrl": "https://mainnet.helius-rpc.com/?api-key=..."
}

Credentials metadata response

{
  "ok": true,
  "credentials": {
    "savedAt": "2026-05-21T12:00:00.000Z",
    "complete": true,
    "devWallet": {
      "configured": true,
      "pubkey": "FullBase58Pubkey...",
      "display": "FullBase58Pubkey..."
    },
    "rpc": {
      "configured": true,
      "provider": "Helius",
      "host": "mainnet.helius-rpc.com",
      "hasApiKey": true,
      "display": "Helius � mainnet.helius-rpc.com � api-key ?"
    }
  }
}

Launch readiness (preflight)

Run before control/start or control/run-once. The dashboard calls this for the Launch readiness checklist. Integrations should treat ready: false as a hard block (same as the UI).

GET /api/projects/:id/preflight

Session auth. No side effects.

{
  "ok": true,
  "ready": false,
  "checks": [
    {
      "id": "mint_set",
      "label": "Mint (CA)",
      "ok": true,
      "detail": "65C5dduDBr9LcAmqrQTaStAKPj3ksfk4i7TTBKKGpump",
      "required": true
    },
    {
      "id": "dev_is_creator",
      "label": "Dev wallet is creator",
      "ok": true,
      "detail": "Dev wallet matches on-chain claim wallet (...)",
      "required": true
    }
  ]
}

Check IDs

idrequiredMeaning
mint_setyesProject has a mint (CA)
mint_formatyesValid base58 Solana address
credentialsyesDev wallet + RPC saved
rpcyesRPC getHealth OK (latency, slot)
mint_onchainyesMint exists on mainnet (SPL or Token-2022)
mint_marketnoDexScreener ticker found (informational)
reward_assetconditionalJupiter swap-route probe for USDC / custom rewards (required); informational for SOL / project-token rewards
dev_is_creatoryesSaved dev pubkey can claim creator fees (bonding-curve creator, PumpSwap coinCreator, fee-sharing admin when applicable, or non-zero vault)
balance_reserveyesDev wallet SOL >= settings.minReserveSol (default 0.05)
balance_recommendednoCovers reserve + minClaimSol buffer
runneryesCloud on-chain engine available

ready is true only when every required: true check has ok: true.

Scheduler control

All control routes require saved credentials, a valid mint, a passing creator-wallet check, and (recommended) preflight.ready === true. The server sets mode to LIVE before starting cycles.

MethodPathDescription
POST/api/projects/:id/control/startStart interval scheduler on mainnet
POST/api/projects/:id/control/stopStop scheduler
POST/api/projects/:id/control/run-onceRun one mainnet cycle now (409 if busy)

First LIVE start: when platform X posting is configured, the API may post one announcement tweet per project (see operator docs). This does not affect cycle logic.

Typical 400 errors: missing credentials, invalid mint, dev wallet not creator, engine not ready. Run preflight first to surface the same checks in one response.

Status, stats, events

GET /api/projects/:id/status

Dashboard snapshot: running, mode, mint, raw stats, and derived publicStats (the same 6 tiles used on the public token page). Also includes credentials, cycleBusy, runner. Includes simRuntime only for legacy SIM projects.

GET /api/projects/:id/stats

Stats for the project's current mode bucket. Returns raw stats and derived publicStats.

GET /api/projects/:id/events?limit=50

Event log for dashboard. limit 1-100. Operator-only log lines are filtered out. Includes POLICY rows when you change public distribution settings (same text as on the token page).

publicStats fields (LIVE, used by token page)

Note: totalDistributedSol is an estimate based on cycle accounting, not a sum of on-chain transfers.

Common raw stats fields (LIVE)

Raw counters are still returned for dashboards and integrations that need deeper detail.

Live stream (SSE)

Server-Sent Events for real-time dashboard updates (status + new log lines). Replaces aggressive polling. EventSource cannot send Authorization, so pass the JWT as ?token= (HTTPS only).

GET /api/projects/:id/stream?token=JWT

Content-Type: text/event-stream. Reconnects automatically; server sends event: rotate about every 9 minutes.

SSE events

eventdata
statusSame shape as GET .../status (without wrapping ok)
events{ events: [{ id, tag, body, severity, at }] }
rotate{ reason: "cycle" } - client should reconnect

Optional: Last-Event-ID header or ?since= for incremental event replay after reconnect.

Widgets (session auth)

CRUD for embeddable overlays. Public live data is under Public feeds (no metrics key in the browser).

MethodPathDescription
GET/api/projects/:id/widgetsList widgets + embedBase URL
POST/api/projects/:id/widgetsCreate (name, config)
GET/api/projects/:id/widgets/:widgetIdGet one widget
PATCH/api/projects/:id/widgets/:widgetIdUpdate name, config, enabled (PUT also accepted)
DELETE/api/projects/:id/widgets/:widgetIdDelete widget

Widget config schema: Widget builder guide.

Share card (session auth)

Parchment-style share images and optional X posting from the dashboard.

MethodPathDescription
GET/api/projects/:id/share-card/bundlePNG (base64) + caption + tweetText
GET/api/projects/:id/share-card/captionCaption and tweet text only
GET/api/projects/:id/share-card.pngRaw PNG image
POST/api/projects/:id/share-card/post-xUpload image and post to X (server credentials; 503 if not configured)

Metrics API keys

Manage read-only keys for external overlays (see Metrics API).

MethodPathDescription
GET/api/projects/:id/keysList keys (no secret values)
POST/api/projects/:id/keysCreate key - returns metricsKey once
DELETE/api/projects/:id/keys/:keyIdRevoke key

Metrics API v1 (read-only)

Dedicated guide: Metrics API (OBS, websites, untrusted environments).

MethodPathAuth
GET/api/v1/projects/:id/metricsX-Metrics-Key
GET/api/v1/projects/:id/events?limit=X-Metrics-Key

Important: stats in Metrics API are LIVE mainnet counters only. mint, settings.tokenSymbol, and settings.eligibilityTiers are included when configured. activeTier summarizes the last LIVE cycle tier: { mcUsd, minTokens, holdCycles }.

Public feeds (no session)

No wallet login and no metrics key. Rate limited and edge-cached where noted.

MethodPathAuthDescription
GET /api/public/widgets/:id/config None Saved widget layout + theme (Cache-Control: max-age=60)
GET /api/public/widgets/:id/live None LIVE counters + recent events for the widget's project (max-age=5)
GET /api/public/ward-roll None All tokens directory - every Latrine Bot project (?faces=1 adds hero avatars)
GET /api/public/latrine/live None Homepage showcase block ($LATRINE only)
POST /api/public/studio/layouts None Create overlay layout for a LIVE project (returns deviceSecret once)
GET /api/public/studio/layouts/:id None Load layout config + streamUrl (max-age=30)
PATCH /api/public/studio/layouts/:id X-Studio-Secret Update layout JSON from Stream Studio

Widget embed: Widget builder guide. OBS overlay editor: Stream Studio guide.

Stream Studio API

Anonymous layout storage for the Stream Studio editor and /stream/?l= OBS page. No session auth - writes require the device secret returned at creation.

POST /api/public/studio/layouts

Body: { projectId, preset?, config? }. projectId must exist. Presets: latrine, pumpfun, minimalism, breach, retro, noir. Response 201: layout, deviceSecret, streamUrl, studioUrl. Rate limit: 20 creates per minute per IP.

GET /api/public/studio/layouts/:id

Public read of sanitized layout config. Used by OBS and Studio when opening ?l=.

PATCH /api/public/studio/layouts/:id

Header X-Studio-Secret: <deviceSecret>. Body: { config, preset? }. Max config size 32 KB. Rate limit: 120 patches per minute per layout id per IP.

Live stats for overlays use GET /api/public/realm/:projectId/live (not Metrics API keys). Stream Studio token search loads the All tokens list via GET /api/public/ward-roll?faces=1 (LIVE projects only).

Distribution transparency API

Token pages read policy state from GET /api/public/realm/:id/live (cached ~45 s; refreshed on policy PATCH). Devs cannot disable these fields or suppress POLICY logging through any dashboard or API setting.

GET .../realm/:id/live - transparency fields

In addition to stats, publicStats, and recent activity lines, the payload includes:

FieldTypeDescription
poolSplitobject{ distributePct, burnPct, holdPct } - current creator fee split (hold = remainder).
holdFundobjectRead-only hold reserve view. heldSol, holdPct, purposeLine, mode (simple, goal, or guaranteed), label, goalSol, showProgress, progressPct. Dex Vault adds purposeLineShort, goalUsd, targetSol, progressGoalLine, and guaranteed (vault pubkey, execution status). Token page shows the card in goal mode (with template) or guaranteed mode. In Dex Vault mode heldSol is vault balance, not stats.totalHeldSol. See Configuration.
publicFeaturesobjectholderRewardChoice, socialClaimBoost, defaultRewardLabel, defaultRewardKind.
policyHistoryarrayUp to 50 POLICY lines from permanent audit storage: { at, body, severity, time } (newest first).
policyAlertobject | omittedLatest POLICY change: { at, body, severity, ageSec } for the sticky banner (stays until the next change).
recent[]arrayActivity log tail; includes POLICY rows with tag, body, severity, at, time.
{
  "ok": true,
  "status": "live",
  "poolSplit": { "distributePct": 100, "burnPct": 0, "holdPct": 0 },
  "holdFund": {
    "mode": "goal",
    "template": "dex",
    "label": "Dex Paid",
    "purposeLine": "Holding for Dex Paid",
    "customLabel": "",
    "goalSol": 4,
    "heldSol": 0.016,
    "holdPct": 10,
    "showProgress": true,
    "progressPct": 0.4
  },
  "publicFeatures": {
    "holderRewardChoice": true,
    "socialClaimBoost": true,
    "defaultRewardLabel": "$TICKER",
    "defaultRewardKind": "SAME"
  },
  "policyHistory": [
    {
      "at": "2026-06-06T04:34:03.000Z",
      "body": "Distribution changed. Holders 61% -> 100%. Burn 0% -> 0%. Hold 39% -> 0%.",
      "severity": "warn",
      "time": "04:34:03"
    }
  ],
  "policyAlert": {
    "at": "2026-06-06T04:34:03.000Z",
    "body": "Distribution changed. Holders 61% -> 100%. …",
    "severity": "warn",
    "ageSec": 120
  },
  "recent": [ … ]
}

Human-oriented rules: Configuration - Distribution transparency.

Dex Vault API (session)

Dashboard-only routes for Dex Prefill and Activate Dex Vault. Require session auth (same JWT as other /api/projects/:id routes). User-facing name: Dex Vault (locked vault - hold fees segregated from dev); persisted API mode: guaranteed.

MethodPathPurpose
POST/api/projects/:id/hold-fund/guaranteed/uploadMultipart kind (icon | header) + file - stores Dex Prefill images in R2; returns public URL.
POST/api/projects/:id/hold-fund/guaranteed/preflightValidate { meta } JSON (description, images, links). Returns preflightStatus and errors.
POST/api/projects/:id/hold-fund/guaranteed/draftSave settings.guaranteedDexDraft while configuring Dex Prefill.
POST/api/projects/:id/hold-fund/guaranteed/resetClear draft and uploaded prefill images before activation only.
POST/api/projects/:id/hold-fund/guaranteed/enableIrreversible. Lock metadata, generate vault keypair, set holdFund.mode: "guaranteed", append POLICY line. Response includes vaultPubkey.

Public image URLs (no auth) for DexScreener order creation:

After activation, hold % slices route to the vault each cycle until Dex payment completes. Hold % cannot decrease below the locked value.

Holder perks API (no session)

Public endpoints for the Holder perk (reward currency choice) and Boost (X post drop) panels on token pages. The same contracts are mirrored under /api/public/latrine/... for the homepage showcase project bound via SHOWCASE_PROJECT_ID. Configure toggles in the dashboard: Holder reward choice, X post boost.

X post boost mechanics

TopicBehaviour
DurationDefault 60 minutes (settings.socialBoostDurationMin). Claim response includes boostUntil (UTC ISO).
One post URL per token, foreverPrimary key (project_id, tweet_id). Same X URL always returns 409 for that token (any wallet), even after expiry or payout. First valid claim wins.
One active claim per wallet per tokenWhile boost_until > now, the same wallet cannot claim another post on this token. 409 includes boostUntil. Limits are per token - other projects are independent.
Claim bodyPOST { tweetUrl, wallet } - the pasted wallet receives boost effects in subsequent cycles. No SIWS.
Repeat boostAfter the hour ends (or after payout), publish a new X post and submit its new URL. Old URLs stay burned.
Eligible holderWeight x socialHolderBoostMultiplier (default 1.15) when holder already passed hold-cycle gate.
Non-holder / gated holderIntro recipient with virtual weight (socialNonHolderWeightRatio x tier minTokens), dev default cohort only.
Lifecyclepending while active - may become paid after a cycle that paid the wallet. Boost applies only while pending and now < boost_until.

Per-token realm (/api/public/realm/:projectId/...)

MethodPathDescription
GET/api/public/realm/:id/liveToken page LIVE payload: stats, activity log, poolSplit, policyHistory, policyAlert
GET/api/public/realm/:id/check-eligibility?wallet=Eligibility lookup for one wallet
POST/api/public/realm/:id/check-eligibilitySame with JSON body { wallet }
GET/api/public/realm/:id/reward-optionsHolder perk enabled flag + SAME/SOL/USDC options + dev default
GET/api/public/realm/:id/reward-preference?wallet=Saved reward kind for a wallet or null
POST/api/public/realm/:id/reward-preferenceSave preference (SIWS proof required)
GET/api/public/realm/:id/social-claimBoost enabled flag + post template
POST/api/public/realm/:id/social-claimClaim boost from tweet URL + wallet
GET/api/public/realm/:id/share-card/bundlePublic share card PNG + caption

Homepage showcase (/api/public/latrine/...)

Same JSON shapes as realm routes above, scoped to the operator-bound showcase project (not a generic project id in the path).

MethodPathDescription
GET/api/public/latrine/liveHomepage ledger snapshot
GET/api/public/latrine/reward-optionsHolder perk options (from KV snapshot)
GET/api/public/latrine/reward-preference?wallet=Prefs stored in showcase KV
POST/api/public/latrine/reward-preferenceSave pref (SIWS)
GET/api/public/latrine/social-claimBoost status + template (showcase project D1)
POST/api/public/latrine/social-claimClaim boost
GET/api/public/latrine/check-eligibility?wallet=Eligibility from KV snapshot
POST/api/public/latrine/ingestOperator push (SHOWCASE_PUSH_KEY) - not browser-facing

GET .../reward-options

{
  "ok": true,
  "enabled": true,
  "options": ["SAME", "SOL", "USDC"],
  "defaultKind": "SAME",
  "defaultRewardKind": "SAME",
  "defaultRewardLabel": "Project token (buyback)",
  "rewardMint": null,
  "ticker": "$TICKER"
}

POST .../reward-preference

Body: { wallet, message, nonce, signature, kind } where kind is SAME, SOL, or USDC. Obtain message + nonce from GET /api/auth/challenge?wallet= (SIWS). The signed message must match the challenge wallet. Returns { ok: true, kind }.

GET .../social-claim

{
  "ok": true,
  "enabled": true,
  "ticker": "$TICKER",
  "mint": "FULL_MINT_BASE58",
  "handle": "@Latrine_bot",
  "template": "$TICKER\nCA: ...\nRunning on @Latrine_bot"
}

POST .../social-claim

Body: { tweetUrl, wallet } where wallet is a base58 Solana pubkey (32-byte). No session or SIWS signature - the server trusts the pasted wallet for this claim only. The tweet must already be public; the Worker fetches text and validates $TICKER, full mint CA, and @handle.

On success (boost window starts immediately, default 1 hour):

{
  "ok": true,
  "status": "pending",
  "boostUntil": "2026-06-05T12:00:00.000Z",
  "message": "Boost active until ... Your wallet will join the next drop."
}

Rules recap: one X post URL per token forever (global); one active claim per wallet per token while the hour runs; different wallets can each claim different posts on the same token; the same wallet can boost other tokens in parallel. Common errors: 400 validation (missing $TICKER, mint, or @handle in tweet, or more than one CA in text), 403 feature disabled, 404 tweet deleted or project not LIVE, 409 tweet URL already used, or wallet already has an active boost on this token, 429 rate limit, 502 could not fetch tweet text.

Rate limits (per IP, per minute)

Errors and limits

Failed requests return JSON:

{ "ok": false, "error": "Human-readable message" }
CodeWhen
400Validation, preflight/creator failure, cannot start
401Missing or invalid session / metrics key
404Project, widget, or key not found
409Cycle already running (run-once); social claim tweet URL already used; or wallet already has an active boost on this token
429Auth rate limit or public widget rate limit
502 / 503Share card X post / media upload failures
500Unexpected server error

Auth challenge: 20 requests per minute per IP; verify: 30 per minute per IP.

Public widget live: 120 requests per minute per IP per widget id.

Metrics API guideGetting startedSecurityMetrics API test page