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).
@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 URL | https://api.latrinebot.com |
|---|---|
| Format | JSON request and response bodies |
| CORS | Enabled for https://latrinebot.com, https://studio.latrinebot.com, and preview *.pages.dev |
| Version | 0.4.0 (GET /api/v1) |
Authentication types
| Type | Header / flow | Used 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.
- New projects are created with
mode: "LIVE". - Client
PATCH /api/projects/:idcannot changemode(onlyname,mint,settings). POST .../control/startandPOST .../control/run-onceforce the project to LIVE if it was still on a legacy mode, then run mainnet cycles.- Before starting, use
GET .../preflight(or the dashboard Launch readiness panel) to confirm mint, RPC, dev wallet, balances, and engine health. - Start and run-once also verify that the saved dev wallet can claim Pump.fun creator fees for the mint (bonding-curve creator, PumpSwap
coinCreatorwhen applicable, or a non-zero creator-fee vault).
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.
| Field | Where | Description |
|---|---|---|
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
| Method | Dashboard button | Account id in API | authProvider |
|---|---|---|---|
| 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
- Returned as
tokenin JSON and set as an HTTP-only session cookie (7 days). - The dashboard also stores the token in
localStorageaslb_session_tokenand sendsAuthorization: Bearer <token>. - Field name
walletin API responses is the account id (pubkey orgoogle:/x:prefix), not always a Solana address. POST /api/auth/logoutclears the session cookie. Clients should delete their stored JWT as well.
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.
GET /api/auth/challenge?wallet=PUBKEY- returnsnonce,message,domain,uri(nonce TTL 5 minutes).- User signs
messagein the wallet (UTF-8). POST /api/auth/verifywithwallet,signature(base58),message,nonce.- 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:
- Browser opens
GET /api/auth/googleorGET /api/auth/x(302 redirect to provider). - User approves at Google or X.
- Provider redirects to API callback:
/api/auth/google/callbackor/api/auth/x/callback. - API creates or loads user, mints JWT, stores it under a one-time
login_codein KV (TTL 120s). - API redirects browser to
https://latrinebot.com/app/?login_code=UUID(orauth_error=...on failure). - Dashboard JavaScript calls
POST /api/auth/complete-oauthwith{ "code": "login_code" }, receivestoken, saves it, calls/api/auth/me.
B. Google
| Start | GET /api/auth/google |
|---|---|
| Callback | GET /api/auth/google/callback |
| Scopes | openid email profile |
| Account id | google:<sub> from Google userinfo |
| Label | Email 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)
| Start | GET /api/auth/x |
|---|---|
| Callback | GET /api/auth/x/callback |
| Scopes | users.read tweet.read |
| PKCE | S256 code challenge (required by X OAuth 2.0) |
| Account id | x:<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.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/projects | Session | List your projects |
| POST | /api/projects | Session | Create project (name, mint required; default mode LIVE) |
| GET | /api/projects/symbol?mint= | Session | Resolve tokenSymbol for a CA |
| GET | /api/projects/:id | Session | Get one project |
| PATCH | /api/projects/:id | Session | Update name, mint, settings (not mode) |
| DELETE | /api/projects/:id | Session | Delete project, stop scheduler, archive credentials |
| POST | /api/projects/:id/tiers/reset-default | Session | Reset 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.
| Method | Path | Description |
|---|---|---|
| POST | /api/projects/:id/secrets | Save or update devWalletSecret and/or rpcUrl |
| GET | /api/projects/:id/credentials | Metadata only (pubkey, RPC host, provider) |
| POST | /api/projects/:id/credentials/test | Ping 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=..."
}
- First save must include both dev wallet and RPC.
- Later updates may send either field alone; omitted fields keep the previous encrypted value.
- Dev wallet must be the Pump.fun launch wallet (or an on-chain claim wallet that can collect creator fees for this mint).
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
| id | required | Meaning |
|---|---|---|
mint_set | yes | Project has a mint (CA) |
mint_format | yes | Valid base58 Solana address |
credentials | yes | Dev wallet + RPC saved |
rpc | yes | RPC getHealth OK (latency, slot) |
mint_onchain | yes | Mint exists on mainnet (SPL or Token-2022) |
mint_market | no | DexScreener ticker found (informational) |
reward_asset | conditional | Jupiter swap-route probe for USDC / custom rewards (required); informational for SOL / project-token rewards |
dev_is_creator | yes | Saved dev pubkey can claim creator fees (bonding-curve creator, PumpSwap coinCreator, fee-sharing admin when applicable, or non-zero vault) |
balance_reserve | yes | Dev wallet SOL >= settings.minReserveSol (default 0.05) |
balance_recommended | no | Covers reserve + minClaimSol buffer |
runner | yes | Cloud 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.
| Method | Path | Description |
|---|---|---|
| POST | /api/projects/:id/control/start | Start interval scheduler on mainnet |
| POST | /api/projects/:id/control/stop | Stop scheduler |
| POST | /api/projects/:id/control/run-once | Run 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)
airdropsCount- Airdrops senttotalDistributedSol- To holders (SOL est.)uniqueHoldersPaid- Holders paidlastEligibleCount- Eligible now (from the last snapshot)burnedDisplay- Tokens burned (human readable)totalClaimedSol- Creator fees claimed
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.
cycles,cyclesAirdropped,errorstotalClaimedSol,totalBuybackSol,totalBoughtTokensUiairdropsCount,uniqueHoldersPaidCount,totalAirdropTokensMillionslastMarketCapUsd,lastPriceUsdlastTierMcUsd,lastTierMinTokens,lastTierHoldCycles- active tier from the last cyclelastCycleAt,lastClaimAt,lastAirdropAt
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
| event | data |
|---|---|
status | Same 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).
| Method | Path | Description |
|---|---|---|
| GET | /api/projects/:id/widgets | List widgets + embedBase URL |
| POST | /api/projects/:id/widgets | Create (name, config) |
| GET | /api/projects/:id/widgets/:widgetId | Get one widget |
| PATCH | /api/projects/:id/widgets/:widgetId | Update name, config, enabled (PUT also accepted) |
| DELETE | /api/projects/:id/widgets/:widgetId | Delete widget |
Widget config schema: Widget builder guide.
Share card (session auth)
Parchment-style share images and optional X posting from the dashboard.
| Method | Path | Description |
|---|---|---|
| GET | /api/projects/:id/share-card/bundle | PNG (base64) + caption + tweetText |
| GET | /api/projects/:id/share-card/caption | Caption and tweet text only |
| GET | /api/projects/:id/share-card.png | Raw PNG image |
| POST | /api/projects/:id/share-card/post-x | Upload image and post to X (server credentials; 503 if not configured) |
Metrics API keys
Manage read-only keys for external overlays (see Metrics API).
| Method | Path | Description |
|---|---|---|
| GET | /api/projects/:id/keys | List keys (no secret values) |
| POST | /api/projects/:id/keys | Create key - returns metricsKey once |
| DELETE | /api/projects/:id/keys/:keyId | Revoke key |
Metrics API v1 (read-only)
Dedicated guide: Metrics API (OBS, websites, untrusted environments).
| Method | Path | Auth |
|---|---|---|
| GET | /api/v1/projects/:id/metrics | X-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.
| Method | Path | Auth | Description |
|---|---|---|---|
| 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:
| Field | Type | Description |
|---|---|---|
poolSplit | object | { distributePct, burnPct, holdPct } - current creator fee split (hold = remainder). |
holdFund | object | Read-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. |
publicFeatures | object | holderRewardChoice, socialClaimBoost, defaultRewardLabel, defaultRewardKind. |
policyHistory | array | Up to 50 POLICY lines from permanent audit storage: { at, body, severity, time } (newest first). |
policyAlert | object | omitted | Latest POLICY change: { at, body, severity, ageSec } for the sticky banner (stays until the next change). |
recent[] | array | Activity 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.
| Method | Path | Purpose |
|---|---|---|
POST | /api/projects/:id/hold-fund/guaranteed/upload | Multipart kind (icon | header) + file - stores Dex Prefill images in R2; returns public URL. |
POST | /api/projects/:id/hold-fund/guaranteed/preflight | Validate { meta } JSON (description, images, links). Returns preflightStatus and errors. |
POST | /api/projects/:id/hold-fund/guaranteed/draft | Save settings.guaranteedDexDraft while configuring Dex Prefill. |
POST | /api/projects/:id/hold-fund/guaranteed/reset | Clear draft and uploaded prefill images before activation only. |
POST | /api/projects/:id/hold-fund/guaranteed/enable | Irreversible. Lock metadata, generate vault keypair, set holdFund.mode: "guaranteed", append POLICY line. Response includes vaultPubkey. |
Public image URLs (no auth) for DexScreener order creation:
GET /api/public/hold-fund/:projectId/iconGET /api/public/hold-fund/:projectId/header
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
| Topic | Behaviour |
|---|---|
| Duration | Default 60 minutes (settings.socialBoostDurationMin). Claim response includes boostUntil (UTC ISO). |
| One post URL per token, forever | Primary 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 token | While 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 body | POST { tweetUrl, wallet } - the pasted wallet receives boost effects in subsequent cycles. No SIWS. |
| Repeat boost | After the hour ends (or after payout), publish a new X post and submit its new URL. Old URLs stay burned. |
| Eligible holder | Weight x socialHolderBoostMultiplier (default 1.15) when holder already passed hold-cycle gate. |
| Non-holder / gated holder | Intro recipient with virtual weight (socialNonHolderWeightRatio x tier minTokens), dev default cohort only. |
| Lifecycle | pending 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/...)
| Method | Path | Description |
|---|---|---|
| GET | /api/public/realm/:id/live | Token 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-eligibility | Same with JSON body { wallet } |
| GET | /api/public/realm/:id/reward-options | Holder 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-preference | Save preference (SIWS proof required) |
| GET | /api/public/realm/:id/social-claim | Boost enabled flag + post template |
| POST | /api/public/realm/:id/social-claim | Claim boost from tweet URL + wallet |
| GET | /api/public/realm/:id/share-card/bundle | Public 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).
| Method | Path | Description |
|---|---|---|
| GET | /api/public/latrine/live | Homepage ledger snapshot |
| GET | /api/public/latrine/reward-options | Holder perk options (from KV snapshot) |
| GET | /api/public/latrine/reward-preference?wallet= | Prefs stored in showcase KV |
| POST | /api/public/latrine/reward-preference | Save pref (SIWS) |
| GET | /api/public/latrine/social-claim | Boost status + template (showcase project D1) |
| POST | /api/public/latrine/social-claim | Claim boost |
| GET | /api/public/latrine/check-eligibility?wallet= | Eligibility from KV snapshot |
| POST | /api/public/latrine/ingest | Operator 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)
reward-options,reward-preferenceGET,social-claimGET: 60reward-preferencePOST: 20social-claimPOST: 10
Errors and limits
Failed requests return JSON:
{ "ok": false, "error": "Human-readable message" }
| Code | When |
|---|---|
400 | Validation, preflight/creator failure, cannot start |
401 | Missing or invalid session / metrics key |
404 | Project, widget, or key not found |
409 | Cycle already running (run-once); social claim tweet URL already used; or wallet already has an active boost on this token |
429 | Auth rate limit or public widget rate limit |
502 / 503 | Share card X post / media upload failures |
500 | Unexpected 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 guide � Getting started � Security � Metrics API test page