HTTP API
The HTTP routes of the Sandbox platform: library, games, likes, season, publishing, plays and AI generation, with parameters, responses and examples.
The platform exposes a small JSON API under /api. Read routes are public and keyless; write routes rely on wallet signatures rather than accounts.
Overview
| Topic | Behaviour |
|---|---|
| Base URL | https://buildsandbox.app |
| Format | JSON in and out. |
| Authentication | No key. Writes that involve a wallet require a message signature (EIP-191) verified on the server. |
| Errors | Matching HTTP status and a { "error": "…" } body. |
| Draft mode | With no storage configured, reads return live: false and empty lists or the templates; writes answer 503. |
tpl- (for instance tpl-race, tpl-hunt). They have boards, but never count for rewards.Library
/api/librarystaticReturns the whole asset catalogue as produced by the pipeline. The route is static (force-static): it is computed at build time and served from cache.
{
assets: {
id: string
name: string
category: 'characters' | 'creatures' | 'vehicles' | 'architecture' | 'nature'
| 'weapons' | 'furniture' | 'props' | 'textures' | 'hdri' | 'effects' | 'audio'
style: 'realistic' | 'stylized' | 'cartoon' | 'lowpoly' | 'voxel'
tags: string[]
source: string
author: string
sourceUrl: string
license: 'CC0' | 'CC-BY' | 'CC-BY-SA' | 'MIT' | 'Other'
attributionRequired: boolean
attribution: string
url: string // GLB, texture set, .hdr or audio file
thumb: string
turntable?: string
polycount?: number
bytes: number
rigged: boolean
animations: string[]
size: [number, number, number]
height: number
importedAt: string
featured?: boolean
maps?: Record<string, string> // textures only
}[]
}curl https://buildsandbox.app/api/libraryconst { assets } = await fetch('https://buildsandbox.app/api/library').then((r) => r.json())
const rigged = assets.filter((a) => a.rigged && a.license === 'CC0')Games
List games
/api/gamesPublished games followed by the templates, each with its play statistics.
modeGameModecreatorstringsort'new' | 'played'Default 'new'played sorts by play count, then by publication date.{
live: boolean // false in draft mode (no storage)
games: {
id: string // on-chain id, "d-<hash prefix>" in draft registry, or "tpl-*"
title: string
description: string
mode: GameMode
creator: string // address, or "sandbox" for templates
cover: string | null
uri: string // where the document lives
hash: string // keccak256 of the canonical document
version: number
publishedAt: number // ms
updatedAt: number // ms
hidden: boolean
onchain: boolean // true once registered in Sandbox.sol
tx?: string
remixOf?: string
plays: number
players: number
}[]
}curl https://buildsandbox.app/api/games?mode=race&sort=playedconst res = await fetch('https://buildsandbox.app/api/games?mode=race&sort=played')
const { games } = await res.json()
console.log(games[0].title, games[0].plays)Get a game
/api/games/[id]idstringrequiredd-… for the draft registry, or tpl-….doc'1'?doc=1, returns only the raw SandboxGame document.{
card: GameCard & { plays: number; players: number; anon: number }
doc: SandboxGame | null
board: { address: string; score: number; at: number }[] // best score per wallet, top 20
}Answers 404 when the game does not exist, or when ?doc=1 is asked and the document is missing. The document format is described in Scene format.
curl https://buildsandbox.app/api/games/tpl-hunt?doc=1const doc = await fetch('https://buildsandbox.app/api/games/tpl-hunt?doc=1').then((r) => r.json())
console.log(doc.meta.mode, doc.entities.length)Likes
/api/likesidsstringids, returns every non-zero count.{ "likes": { "tpl-race": 12, "qt-t-rex": 4 } }Counts come from a single storage listing, kept 30 seconds in memory and served with cache-control: public, max-age=20.
/api/likesBody { id, address? }. One like per visitor and target: the wallet address when given, otherwise a salted hash of the IP. Answers { ok: true }, or 400 for a bad id.
Season and rewards
/api/seasonThe current epoch (a 24-hour window), the standings of the current and previous epochs, the settlements already posted and, when the contract is deployed, its treasury state.
{
live: boolean
epoch: number
start: number // ms
end: number // ms
current: Standing[] // sorted by weight
previous: Standing[]
settlements: {
epoch: number
amountWei: string
totalWeight: number
rows: { id, title, creator, weight, players, plays }[]
tx: string
at: number
}[]
treasury: { balance: string; reserved: string; epoch: number } | null
}
interface Standing {
id: string; title: string; creator: string; mode: GameMode
cover: string | null; weight: number; players: number; plays: number
}Weight is expressed in hundredths (a weight of 1 is 100). The formula is explained in Epoch rewards.
Publishing
Publishing takes two calls. The studio chains them for you; they are documented here for third-party tools.
1. Upload the document
/api/games/uploadBody { doc, cover? }, where cover is a PNG data URL of 2 MB at most. The document is validated, hashed and stored under its hash. Response { hash, uri, cover }.
| Status | Cause |
|---|---|
| 400 | Bad JSON, or document refused by validation (the message says why). |
| 413 | Document over 40 MB or cover over 2 MB. |
| 503 | Draft mode: no storage configured. |
2. Register the card
/api/games/registerMakes the game visible on the platform. Two paths depending on whether the on-chain registry is deployed.
Body { hash, address, signature, cover?, remixOf? }. The signature covers the message below; the server also checks the token holding threshold once the token is live. The game id becomes d- followed by the first ten characters of the hash.
Sandbox publish
title: <title>
hash: <0x…>Body { hash, gameId, tx?, cover?, remixOf? }. The wallet first called publish or update on Sandbox.sol; the server reads the game back on-chain and refuses when the hash does not match. Title, creator and version come from the contract.
| Status | Cause |
|---|---|
| 400 | Missing hash, missing signature, or on-chain hash mismatch. |
| 401 | Bad signature. |
| 403 | Token balance too low to publish. |
| 404 | Document not uploaded, or game not found on-chain. |
| 503 | Draft mode. |
The publishing flow in the studio
Plays
/api/playsSent at the end of every run. Tied to a wallet — by the sitting’s session signature or by the signature of the run itself — it counts fully toward the game’s weight for the current 24-hour epoch; only the run signature can improve the wallet’s best score on the board. Anonymous, it counts a tenth.
gamestringrequiredscorenumberrequiredatnumberrequiredtimenumberwonbooleanmodestringaddressstringsignaturestringsession{ at, signature }Sandbox session
game: <id>
at: <at>
Sandbox play
game: <id>
score: <score>
at: <at>{ "ok": true, "live": true, "signed": true, "ranked": true, "improved": false }Errors: 400 malformed or stale play, 401 bad signature or stale session, 404 unknown game. In draft mode the route answers { ok: false, live: false }.
AI 3D generation
Paid, when the token launches
403 token_not_live. See AI 3D generation./api/gen3dBody { kind, prompt?, imageUrl?, quality?, textured?, rig?, provider?, address, payTx }. payTx is the token transfer to the treasury; it is checked on-chain and works once. Response { job, quota }.
| Status | error | Cause |
|---|---|---|
| 400 | prompt_required / image_required | Missing prompt or image. |
| 401 | wallet_required | Missing or bad address. |
| 402 | payment_required / payment_used / payment_pending / payment_failed / payment_mismatch | Payment missing, already used, unconfirmed, failed or too small. |
| 403 | token_not_live | Generation not open yet. |
| 429 | quota_exceeded | Daily quota reached. |
| 503 | no_provider | No Meshy or Tripo key configured. |
/api/gen3d?owner=0x…Lists a wallet’s jobs and returns { jobs, providers, quota, limit, open }: which providers have a key, the quota used and whether generation is open.
/api/gen3d/[id]Polls the provider while the job runs, then returns { job } with status (queued, running, done, failed), progress from 0 to 100 and, when done, result.url pointing at the GLB copied into our storage. 404 not_found for an unknown id.
/api/gen3d/uploadReference image for image-to-3D: multipart (field file) or JSON { data: "data:image/png;base64,…" }. PNG, JPEG or WebP, 8 MB max. Response { url, id, bytes }; errors 400 file_required, 413 too_large, 415 unsupported_type.