Builda SDK
SDK 0.12.3 · Updated 2026-08-25 中文
Contents

Builda SDK

For developers publishing games to the Builda platform (Godot Web / HTML5 / Unity WebGL): what the Builda SDK provides, how to integrate and use it, and the rules you must follow. For packaging and upload rules see h5_bundle_spec.md; this guide covers only the SDK itself.

Copy for AI

Install Builda game tools from /agent/setup.md

What is Builda SDK

Builda SDK is the only interface layer between your game and the Builda platform (the Builda App host). It is exposed to the game as the global object window.Builda. All three engines share the same JS API; Godot and Unity each have an official wrapper layer (a GDScript addon / a C# static class) with semantics identical to the JS API.

ModuleCapabilities
runtimeReady signal, quit, safe area / capsule button avoidance, current language
whoami / requestTokenCurrent player identity, game-scoped short-lived service token
getRoleInfo / getServerTimeOther players' public nicknames and avatars, Builda server-authoritative time
privateKVPer-player private cloud save (current game × current player)
assetsURL resolution for the platform asset pack (assets.zip)
audioBGM / sound effects / mute / vibration / microphone permission
micMicrophone PCM stream primitives (voice-controlled gameplay)
payIn-app purchase payment panel
adRewarded video ads
inputPlatform-native text input panel
rankLeaderboard submission and queries
eventsSubscription callbacks: app going to background / returning to foreground, pause panel shown / closed

Two builds: mock (development) and runtime (production)

The SDK has two implementations with an identical API surface, so game code never needs to tell the environments apart:

Key contract: your game zip does not bundle the SDK file itself. The <head> of index.html references it with a root-relative path: <script src="builda-sdk.js"></script>. Locally, builda dev automatically serves the mock build as a fallback; in production, Builda App downloads the production build from the CDN according to the sdk contract in the release manifest and injects it at the same path. Upload validation strictly checks for this reference; build tools (vite/webpack) must not bundle it as a module — it must remain an external <script> tag.

Unified return format

Every asynchronous API returns a Promise that resolves to a unified Result structure; it never throws:

{ "ok": true,  "data": { } }
{ "ok": false, "error": { "code": "BAD_KEY", "message": "..." } }

A few purely local data APIs return synchronously and unwrapped (no Result wrapper, no await): runtime.safeArea(), runtime.capsuleMenuRect(), runtime.language(), assets.url() / assets.baseUrl, and mic.read() (real-time data must be available within the frame). Most async APIs accept opts.timeoutMs (default 120 seconds); on timeout they return code: "PENDING".

Besides request-style APIs there is a set of subscription callbacks, Builda.events.on/off (events the host pushes to the game: app going to background / returning to foreground, pause panel shown / closed). See "events: subscription callbacks" below.

Integration (by engine)

Prerequisite for all three engines: the project-level toolchain (./.builda-agent/builda) has been installed via the setup task on the home page / upload page. If it isn't installed yet, click "Copy for AI" in the top-right corner and send the setup task to the AI in your project.

HTML5 (pixi / phaser / Cocos / plain canvas)

  1. Run ./.builda-agent/builda sdk install in the project root — the mock SDK, builda-sdk.d.ts and the test shell are installed to .builda-agent/sdk/web/ (regenerable, do not commit).
  2. Add <script src="builda-sdk.js"></script> to the game HTML <head> (root-relative path; it must survive into the build output).
  3. Call window.Builda.* directly. At the very start of the game, you must call Builda.runtime.ready() (the first thing in your entry script).
  4. TypeScript projects can add builda-sdk.d.ts to tsconfig include to get type hints.
  5. Local verification: ./.builda-agent/builda dev --web dist, then check the game in the test shell at the printed dev-url.
Builda.runtime.ready();                                 // must be called at game start (force-closed if not received within 30 s)

const who = await Builda.whoami();
if (who.ok) console.log(who.data.name);

await Builda.privateKV.set("save_slot1", bytes);        // bytes: Uint8Array
const r = await Builda.privateKV.get("save_slot1");     // r.data === null means the key does not exist

Godot (Web export)

  1. In the Godot project root (containing project.godot) run ./.builda-agent/builda sdk install and ./.builda-agent/builda sdk check. This installs addons/builda/ (the SDK wrapper builda.gd, mock JS, examples), copies the recommended wrapper BuildaClient.gd into scripts/ and registers it as an Autoload, and writes the SDK reference into the Web export preset's html/head_include.
  2. The result model is async request/signal: each call returns a request_id, and the result comes back via the sdk_result(request_id, result) signal (result is the Dictionary form of the unified Result):
    var builda = BuildaClient.builda
    var rid := builda.whoami()
    # match the result by request_id in the sdk_result callback
    
    builda.private_kv_set("save", var_to_bytes(save_dict))
    # reading side: bytes_to_var(builda.private_kv_bytes(result.data))
    Byte values (privateKV, mic samples) cross the bridge as base64; restore them with private_kv_bytes() / mic_samples(). Use var_to_bytes() / bytes_to_var() as the save format.
  3. At the very start of the game, you must call builda.runtime_ready() (the auto-installed BuildaClient Autoload already calls it as the first thing in _ready()).
  4. In the editor / non-Web exports every call returns UNSUPPORTED (no crash); behavior must be verified in a browser after a Web export, using builda dev --web build/web.
  5. Do not manually mix addon / JS / d.ts files from different versions; if versions are out of sync, rerun sdk install. scripts/BuildaClient.gd belongs to your project — modify it as needed; upgrades won't overwrite it.

Unity (WebGL export)

  1. Run ./.builda-agent/builda sdk install in the Unity project root — the C# wrapper and jslib bridge are installed to Assets/Builda/, and the Builda WebGL template to Assets/WebGLTemplates/Builda/.
  2. In Player Settings → Resolution and Presentation → WebGL Template select Builda (the template already contains the SDK reference; custom templates must add it to <head> themselves).
  3. Call the static class BuildaSDK in the Builda namespace; async APIs take callbacks:
    using Builda;
    
    BuildaSDK.RuntimeReady(_ => {});                   // must be called at game start (force-closed if not received within 30 s)
    BuildaSDK.Whoami(result => {
        if (result.Ok) Debug.Log(result.DataMap["name"]);
    });
    BuildaSDK.KvSet("save_slot1", bytes);              // byte[]; the SDK bridges it as base64 internally
    BuildaSDK.KvGet("save_slot1", r => {
        var data = BuildaSDK.KvBytes(r.Data);          // null = key does not exist
    });
    var lang = BuildaSDK.RuntimeLanguage();            // synchronous API, returns directly
  4. In the editor / non-WebGL platforms every call returns an UNSUPPORTED failure (no exception). Verify the same way: WebGL export + builda dev.

Capabilities and usage rules

runtime: ready, quit, avoidance, language

whoami and requestToken

Hard rule: keep the token in memory only. It is strictly forbidden to write it to privateKV, local files, URLs, logs or saves.

getRoleInfo and getServerTime: other players' public profiles, authoritative time

const profiles = await Builda.getRoleInfo(["11299825", "11299826"]);
if (profiles.ok) {
  const p = profiles.data["11299825"];           // may be undefined: not found this time
  showOpponent(p ? p.userName : "Player", p ? p.userImg : "");
}

const now = await Builda.getServerTime();
if (now.ok && now.data.serverTimeMs >= dailyResetAtMs) refreshDailyTasks();

Rule: public profiles are for display within the current session only — don't write them into saves or build your own player database from them. Base time decisions on serverTimeMs, but final outcomes involving rewards, rankings and payments are still validated by the platform server.

playerData: observe your stored Player Data

Builda.playerData.getMany(["coins"]) requires non-empty keys; listKeys() returns the directory in data.meta.keys with values={}. Both return {meta,values,source}. Published games can read their own data online without joining a room; draft trials are unsupported. Godot: player_data_get_many / player_data_list_keys. Unity: PlayerDataGetMany / PlayerDataListKeys.

This is a possibly stale, non-authoritative stored observation. Neither revision nor source guarantees freshness. Online game server actions decide rewards, deductions, stealing and payments. Never overwrite a save from this read or replay a successful action because a later read is stale. Failure is not an empty save or zero balance; label retained displays as possibly stale / refresh failed.

Missing keys are null; tombstones are entries with a null value. Values retain a {type,data} tree: int64 and revisions are decimal strings, bytes are Base64, objects recursively contain typed fields. schemaVersion is a uint32 number or null. Do not convert int64 to Number. This is independent of privateKV; never put server secrets in readable fields. The local mock has a separate coins=0 fixture and sends no real HTTP requests.

privateKV: player cloud saves

Private save storage for the current game × current player. Values are opaque byte streams (Uint8Array in JS, PackedByteArray + var_to_bytes() in Godot, byte[] in Unity). API: get / set / remove / getMany / setMany / removeMany.

ItemRule
Key format^[A-Za-z0-9_:-]{1,64}$ (new code should use only letters, digits, _ and -)
Single value≤ 32KB (measured in decoded bytes)
Batch≤ 32 keys per call (the SDK does not split batches automatically; exceeding this returns BATCH_TOO_LARGE)
Total quota≤ 100 live keys per player per game (no aggregate byte quota)
Write rateServer-enforced at about 60 writes/minute; a batch counts as 1 (not simulated by the mock)
ConcurrencyLast write wins; no versioning/CAS
get returns nullThe key does not exist (empty bytes are a valid value; deletion requires an explicit remove)
setManyThe whole batch is validated; any violation rejects the entire batch. Writes are best-effort, not transactional
removeManyIdempotent for keys that don't exist

Usage rules

assets: platform asset pack

Large assets (audio, 3D models, etc.) are not bundled into the main game package; they are packed separately into assets.zip and uploaded via builda assets upload. It contains two top-level subtrees:

Limits: ≤ 32MB per file, ≤ 320MB total uncompressed, ≤ 10000 files. Scripts/executable content (.js / .wasm / .html) and fonts are not allowed.

audio: BGM / sound effects / vibration

await Builda.audio.playBGM("audio/bgm/main.mp3", { loop: true, volume: 0.8 });
await Builda.audio.stopBGM();
await Builda.audio.playSFX("audio/sfx/hit.ogg", { sessionId: "hit", volume: 1 });
await Builda.audio.setMuted(true, "bgm");     // group: "bgm" | "effect"
await Builda.audio.vibrate(2);                // 1 light / 2 medium / 3 heavy

mic: voice control

For voice-controlled gameplay (volume / blowing / pitch), prefer the official built-in component BuildaMic (shipped with the SDK, same design for Godot and Unity) instead of using the low-level primitives directly:

When using the low-level primitives directly (audio.requestMicrophone + mic.start/read/stop, for HTML5 without the built-in component), the core constraint is two gestures:

  1. In the callback of the 1st tap (an "Enable voice control" button), call audio.requestMicrophone() to request permission. available: false = the user denied it or there is no mic: show a clear blocking message and allow retrying (no black screen / hang / silent failure); an alternative no-mic gameplay mode is optional, not required.
  2. After the permission result comes back, don't call mic.start() directly — the permission callback is not in a gesture context (Godot signals are dispatched via per-frame polling and inevitably leave the gesture context), so the AudioContext would be blocked by the autoplay policy with no way to recover. Stop at a "tap again to start" prompt and call mic.start() in the callback of the 2nd tap.
  3. Then call mic.read() each frame to synchronously pull mono float32 PCM ([-1,1]), and call mic.stop() when you no longer need it.

Hard rule: PCM may only be analyzed locally in real time. Recording, retaining or uploading it is forbidden and will be treated as a review incident.

iap: in-app purchases

// grantBy declares the expected fulfillment path; it doesn't override the order's frozen config — actual fulfillment follows grantBy in the response
const r = await Builda.iap.showPayPanel("coin_pack_1", { grantBy: "client" });
// r.data = { success, orderId, grantBy }   success only means payment succeeded; the order stays in Paid
// r.data.grantBy === "client": fulfill idempotently by orderId, then
await Builda.iap.claim(r.data.orderId);
// r.data.grantBy === "room": hand orderId to Room doOrder; the client neither fulfills nor claims
// { orderId, status: "delivered" } (idempotent)
const pending = await Builda.iap.getPendingOrderIds();
// pending.data = { orderIds: string[] }
const r = await Builda.ad.showRewardAd("revive");
// r.data = { success, transId }

input: text input

In a mobile WebView, text boxes drawn by the game can't get the system IME (no candidate list, cursor or selection). For all text input — naming, entering room codes, etc. — call:

const r = await Builda.input.showInputPanel({ placeholder: "Enter a nickname", defaultValue: "", maxLength: 16 });
// confirmed: { confirmed: true, text }   cancelled: { confirmed: false, text: "" }

rank: leaderboards

Leaderboards must first be declared in rankBoards of builda.publish.json and take effect after the release passes review (each board has rankId/displayName/sortType/cycleType/minScore/maxScore, up to 100 boards; sortType: asc|desc, cycleType: day|week|month|forever).

await Builda.rank.submitScore("weekly_score", 1234);
const r = await Builda.rank.getRankList("weekly_score", { limit: 50 });
// r.data = { rankId, list: [{rank, score, player}], self: {rank, score} | null }

events: subscription callbacks (app foreground/background, pause panel)

Request-style APIs are "the game asks, the platform answers"; events are "the platform pushes, the game receives". There are currently two events.

const off = Builda.events.on("appVisibility", ({ visible, source }) => {
  if (!visible) pauseGame();   // app minimized / backgrounded / screen locked
  else resumeGame();           // back in the foreground
});
// off();                       // unsubscribe when needed; Builda.events.off("appVisibility") clears all subscriptions

Builda.events.on("pausePanel", ({ visible }) => {
  if (visible) pauseGameplay();   // host "Paused" panel shown (same panel as capsule ✕ or runtime.quit())
  else resumeGameplay();          // panel closed without quitting
});

online: multiplayer (Builda Online)

If you need multiplayer matches, rooms or an authoritative server, networking is provided by Builda Online, released alongside this SDK at the same version: Builda Online user documentation. After completing the Builda integration on this page, have your project AI run ./.builda-agent/builda sdk install --with-online (CLI 0.4.56+): the Online Skill, client SDK, Room SDK and local mock are all installed at this SDK's version and upgraded together by sdk upgrade. AI-readable setup instructions: https://ai.builda.game/online/releases/0.12.3/setup.md.

Local development and debugging

The builda dev test shell

./.builda-agent/builda dev --web <build-dir>          # e.g. dist, build/web
./.builda-agent/builda dev --web build/web --safearea 44,0,34,0   # simulate a notch + bottom home bar

Open the printed dev-url. The test shell provides: portrait/landscape switching, notch/safe-area simulation, capsule button simulation, payment/ad mock modals (simulating host panels overlaid on the WebView), "Deny mic" / "Mic interrupt" toggles, and a "Background" button (simulating host foreground/background pushes); the pause panel opened by the capsule button also simulates events.pausePanel.

Mock environment URL parameters

ParameterEffect
builda_mock_player / builda_mock_nameSwitch the mock player identity (KV is isolated per player)
builda_mock_lang=es-419Override the value returned by runtime.language() to test language fallback
builda_mock_safearea=44,0,34,0Same as --safearea; injects the safe area (top,right,bottom,left)
builda_mock_mic=denySimulate the user denying the microphone

Mock vs. real device differences

Passing local tests ≠ working on a real device. The mock verifies API shapes and UI flows; real identity, KV, payment, ad and leaderboard semantics can only be verified by playtesting a draft in the Builda App pending-release area.

Capabilitymock (builda dev)Real device (Builda App)
IdentityFake player from URL parametersReal logged-in player
privateKVlocalStorage (same quota checks; write rate not enforced)Host persists to server/local storage; write rate 60/min
Payments/adsModal where you click success/failure manuallyReal host panels/ads
AudioBrowser Audio (subject to autoplay limits)Host-native, no autoplay limits
LeaderboardsNo board config validation, fixed fake opponentsAuthoritative validation against published rankBoards
requestTokenFake token, can't reach real servicesHost exchanges a real token with the platform
events.appVisibilityShell "Background" button simulates host pushes; switching tabs uses the browser fallbackHost onAppEvent pushes + WebView fallback
events.pausePanelPause panel opened by the shell's capsule button / "Resume"Host "Paused" panel shown/closed pushes
SDK filedev serves the mock build as a fallbackHost injects the production build from the CDN per the manifest

Pre-release checklist

Common AI commands

Once the toolchain is installed, just say these to your agent (Claude Code / Cursor, etc.) for the following tasks:

CommandEffect
Open local previewStarts the local mock page so you can preview the in-App experience in a desktop browser
Upload draftUploads the game to Builda App, where you can then publish it
Deploy server (multiplayer games)Uploads and activates the Room server source, carried out by the Builda Online Skill; see Builda Online user documentation

Cautions and hard rules

Hard rules (violations lead to review rejection or are treated as incidents):

  1. Don't bypass the SDK to call the host bridge directly — game code must never touch window.__cysjHost, flutter_inappwebview.callHandler or any low-level App bridge method.
  2. Tokens stay in memory only — the token from requestToken must never be written to privateKV, local files, URLs, logs or saves.
  3. Microphone PCM may only be analyzed locally in real time — recording, retaining or uploading it is forbidden.
  4. Never fake success without a host — payments/ads returning HOST_UNAVAILABLE in a browser is correct behavior; don't paper over it in the game as "purchase succeeded".
  5. Don't modify SDK distribution files — builda-sdk.js, addons/builda/ (except mic/web/) and Assets/Builda/ are overwritten wholesale on upgrade by sdk install / builda check; the only things you own are scripts/BuildaClient.gd, the mic analysis worker directory and your own game code.

Persistent identifiers (think carefully before changing)

What the platform handles (so the game shouldn't)

Common error codes

codeMeaning
HOST_UNAVAILABLENo Builda App host (the normal failure state when opening the runtime build directly in a browser)
PENDINGTimed out waiting for the host to respond (default 120s; pass timeoutMs to change)
BUSYA single-instance panel is already open (e.g. calling input again while its panel is still open)
UNSUPPORTEDEngine wrapper called outside a Web/WebGL environment
BAD_KEY / BAD_VALUE / BAD_PAY_ID / BAD_RANK_ID / BAD_SCOREInvalid argument format (local pre-validation by the SDK)
VALUE_TOO_LARGE / BATCH_TOO_LARGE / QUOTA_EXCEEDEDprivateKV limit exceeded
RANK_NOT_FOUND / SCORE_OUT_OF_RANGELeaderboard not published / score out of range (authoritative host validation)
PERMISSION_DENIED / MIC_UNAVAILABLE / MIC_NOT_STARTEDMicrophone permission denied / unavailable / read before start
WORKER_LOAD_FAILEDThe BuildaMic analysis worker wasn't shipped with the package
JS_ERROR / BAD_RESULT / BAD_RESPONSEBridge-layer error (usually an integration mistake; check the SDK version and how you're calling it)

Further reading

DocumentContents
h5_bundle_spec.mdGame zip packaging spec (entry point, exclusion list, per-engine validation rules)
md_persistent_ids.mdFull contract for persistent identifiers (KV key / payId / rankId)
md_rankboards.mdFull contract for leaderboard manifest configuration
md_bundle_size.mdPackage size reduction and res/** asset splitting in practice
md_godot_i18n.md / md_unity_webgl.mdLocalization and fonts; Unity WebGL export
md_mobile_perf.mdMobile performance / dynamic DPR
Builda Online user documentationMultiplayer: player experience choices, matchmaking and rooms, Builda App's built-in room chat and invites, usage limits; AI installation: builda sdk install --with-online, instructions at https://ai.builda.game/online/releases/0.12.3/setup.md

The md_* topic documents are published with SDK 0.12.3 at https://ai.builda.game/game-sdk/releases/0.12.3/ and are also installed into your project via the guidance/ folder of each of the three engine packages; h5_bundle_spec.md ships with the Agent toolchain.