The Spawn SDK connects a game made in any engine to Spawn: the player, sign-in, saves that follow them to every device, leaderboards, achievements, purchases in spoins, friends, lobbies and player-made levels. It is one file, spawn.js, and it works the same in plain JavaScript, Phaser, three.js, Godot and Unity.
Already on Wavedash's SDK? Publish your game as it is. Spawn puts its own script in your page in place of Wavedash's, and it answers every Wavedash call your game makes, in JavaScript, in Godot (Wavedash's Godot plugin) and in Unity (Wavedash's Unity package). You change no code. Then declare your achievements, boards and items on Spawn (below).
spawn.js into your game's page before your first line. window.Spawn is there when your code starts, and Spawn.player already names the person playing.spawn.js yourself. The SDK plays alone: Spawn.player is a local guest from the first line, saves and scores stay in this browser, a buy grants its item at once for free, and Spawn.auth.signIn() makes the local player a member. So your grant code and your sign-in button run before you publish. Calls that need Spawn (friends, lobbies) fail with code unavailable. The same code runs in both places.Files:
Spawn and WavedashEvery call returns a Promise. A failed call rejects with an error whose code says why: signed_out, not_found, conflict, too_large, too_many, not_declared, cancelled, bad_args, unavailable.
const { player } = await Spawn.ready(); // { id, name, handle, avatarUrl, guest }
The player. Spawn.player is { id, name, handle, avatarUrl, guest }. id is the same in every Spawn game and never changes for this person. A guest plays without an account; their saves, scores and achievements follow them when they sign in. Where the account already has a save of that name, or state in this game, the account's stays: keep the guest's values in memory and write the better ones after the sign-in.
Spawn.auth.signIn() opens Spawn's sign-in for a guest and resolves with the signed-in player. Call it from a button the player presses. If the player closes the sign-in, the call fails with cancelled.Spawn.on('player', fn) hears the person change (they signed in or out). Read their saves again then. One sign-in both resolves signIn() and fires player, so do that work in player alone.Saves follow the player to every device and survive a reload. A save is a name and a value: text, a JSON value, or bytes (Uint8Array). Up to 2 MB a save and 64 saves a player in your game. A name is a path of up to 256 characters with / between its parts (saves/slot1.json); a leading / is dropped.
Spawn.saves.set(name, data) writes it and resolves { name, version, size, updatedAt }. Pass { version } as a third argument to write only over the version you read ({ version: 0 }: only when no save of that name stands): if the save changed on another device meanwhile, the call fails with conflict.Spawn.saves.get(name) resolves { name, version, size, updatedAt, data }, or null. data comes back as you gave it.Spawn.saves.list({ prefix }) lists { name, version, size, updatedAt } (a prefix like saves/ lists one folder), and Spawn.saves.delete(name) removes one.State is a few named values about the player (numbers, short strings, booleans) that Spawn can read: your boards rank them.
Spawn.state.set({ bestLap: 63.2, level: 4 }) (null removes a value) and Spawn.state.get().Leaderboards rank players on your game's page and in your game. A board ranks one state value of each player (by default the value named like the board), highest first or lowest first. Places are exact to #100; past it a player reads a band ("top 10%"). Spawn shows no count of a board's players.
Spawn.leaderboards.submit(board, value, { keepBest, metadata }) posts the player's score into the board's value. Spawn keeps their best, in the board's direction, unless keepBest: false. It resolves { board, value, best, changed, rank, submittedRank, band }: rank is the player's place (null past #100, where band is 10, 25 or 50), submittedRank where the score just posted stands. metadata is small data the entry carries (up to 16 keys of strings, numbers or booleans).Spawn.leaderboards.top(board, { limit, offset, friends }) resolves { board, entries: [{ rank, player, value, at, metadata }] }, at most to #100. friends: true ranks the player's friends and the player among themselves.Spawn.leaderboards.around(board, { ahead, behind, friends }) is the rows around the player, with the player's own (mine). Spawn.leaderboards.mine(board) is the player's own row and place.Spawn.leaderboards.list() and get(board) read your boards; ensure(board, { name, direction, format }) makes one as your game runs, when you did not declare it.Achievements show in the player's den, as a trophy that arrives for them, and on your game's page with the share of players who hold each one.
Spawn.achievements.unlock(key) resolves { key, unlocked, fresh, declared }. fresh is true the first time. Pass an array of keys to unlock several in one call. A key you have not declared yet is kept: the player holds it from the moment you declare it.Spawn.achievements.list() is every achievement with the player's own: { key, name, description, hidden, unlocked, unlockedAt, rarity }. A hidden one the player does not hold has no name or description yet. rarity is { word, rare, percent }, the word Spawn shows beside it ("12%", "<1%", "Rare", "be the first").Purchases in spoins. Spawn draws the purchase sheet; your game never handles money. You keep half of each sale, paid out like any Spawn game's earnings.
Spawn.store.items() is what your game sells, priced in spoins.Spawn.store.buy(key) opens Spawn's purchase sheet and resolves { bought, purchase }. A guest signs in first: the buy opens Spawn's sign-in, then the sheet. A sheet or a sign-in the player closes resolves { bought: false } about a second after it closes. Your own buys of your game are free tests.Spawn.on('purchase', fn) hears each purchase that settled: grant it there. An unlock is the player's for good; read Spawn.store.owned() (the keys of the unlocks the player owns) at each start. A consumable arrives again at each load until you call Spawn.store.fulfill(purchase.id), so save the grant first, then fulfill.files below). Fetch them with fetch() after the player owns the unlock: Spawn sends the player's pass with each request your game makes for its own files. Without it the answer is 403 with { "contentIdentifiers": [...] }, the unlocks the player lacks.Your own server. Spawn.auth.token() is a short-lived JWT that proves who plays: sub is Spawn.player.id, aud is your game's world id, signed RS256 by keys at https://www.spawn.co/api/.well-known/jwks.json (issuer https://www.spawn.co). Each purchase carries receipt, a JWT with header typ spawn-purchase+jwt and the claims sub, aud, event (purchase.completed or purchase.refunded), purchaseId, ptype (DURABLE or CONSUMABLE), contentIdentifier, priceSpoins, env (PLAYTEST for your own free test buys) and created. Verify it like the token, then grant each purchaseId once. With a webhookUrl declared, Spawn posts { "receiptJwt": "…" } to it for each purchase and refund, and tries again for about three days until it answers 2xx within 10 seconds. Your server marks a consumable granted with POST https://www.spawn.co/api/outside/purchases/<purchaseId>/fulfill and { "receiptJwt": "…" }.
Friends and presence. Spawn.friends.list() is the player's Spawn friends. Spawn.presence.set('Level 3') is a short line friends see beside your game's name.
Lobbies and messages. Spawn.lobby.create({ max, visibility }), join(id), leave(), get(), list(), send(data, { to }), setData({...}), invite(friendId) (no id opens Spawn's invite sheet), link() and say(text) (a line in the chat Spawn shows beside the game). Events: lobby.changed, lobby.message, lobby.kicked, lobby.invited. Spawn.launch.lobby names the lobby an invite opened.
What players make. Spawn.ugc.create({ type, title, description, visibility, data }) shares something the player made. type is level (the default: anything other players load and play, a level, a map, a mod), picture, clip, replay (your game's own record of a run; it stays off your game's page on Spawn) or other. visibility is public (the default) or private (the player alone). data is the file: text, a JSON value, bytes or a Blob (canvas.toBlob), up to 64 MB, given back as you gave it. It resolves the item: { id, type, title, description, visibility, by, mine, state, held, file, createdAt, updatedAt }.
state is held, and held says why in a sentence you can show the player.signed_out, and their items follow them when they sign in.Spawn.ugc.list({ type, by, mine, search, cursor, limit }) is your game's items, newest first: { items, cursor }, with cursor null on the last page. mine: true lists the player's own, private ones too; by is a player's id; search finds words in titles.Spawn.ugc.get(id) is one item with its file as data. update(id, changes) and delete(id) change the player's own items; a deleted item is gone everywhere.Spawn.ugc.report(id) lets the player report someone's item: Spawn asks them on its own card.Spawn.launch.ugc at start (Wavedash: getLaunchParams().ugc) and load it with Spawn.ugc.get.The frame. Spawn.loading(0.5) and Spawn.loaded() report your own loading. They are optional: Spawn's cover stands until your game first draws, with or without them. Spawn.fullscreen() takes the whole screen (call it from a tap or a key press). Spawn.openLink(url) copies a link for the player, since your game cannot leave its page.
Achievements, boards and items exist once you declare them. Declare them when you publish (--achievements <file> and --store <file> on the publish command, below) or any time after, with the same publish script and token you published with (the script reads your token from ~/.config/spawn/token). After publishing, send them like this:
curl -fsSo /tmp/spawn-publish.mjs https://www.spawn.co/outside/publish.mjs
node /tmp/spawn-publish.mjs call PUT /api/outside/games/<appId>/progress '{
"achievements": { "first-run": { "name": "First run", "description": "Finish one run." } },
"leaderboards": { "distance": { "name": "Distance", "direction": "desc" } }
}' --site https://www.spawn.co
node /tmp/spawn-publish.mjs call PUT /api/outside/games/<appId>/store '{
"buyables": { "gold-runner": { "name": "Gold runner", "description": "A gold coat for your runner.", "priceSpoins": 50, "once": true } }
}' --site https://www.spawn.co
An achievement is { name, description, hidden }: a name of up to 40 characters and a description of up to 100. A board is { name, state, direction, format }: state names the player's state value it ranks (the board's own key when left out), direction is desc (highest first) or asc (lowest first, for times), and format is number or time (seconds, shown as 1:02.34). The answer names what Spawn lists and what it could not list, and why. At publish time the same file can ride the publish script: write { "achievements": …, "leaderboards": … } to a file and add --achievements <file>. GET /api/outside/games/<appId>/progress reads back what your game declares.
once: true is an unlock, bought one time. Without it the item is a consumable. An item may carry imageUrl, the full https address of its picture anywhere on the web: Spawn's sheet draws it outside your game's box, so it need not be a file of your build. Add "webhookUrl": "https://…" to the store call to name your server. At publish time the same list can ride the publish script: write it to store.json (the buyables object alone) and add --store store.json, with --webhook https://… for your server. In store.json an unlock may also carry "files": ["full-version/**"]: the files of the build only its owners can fetch (** any folders, * any name; never your first page). A call to a key you did not declare fails with not_declared. A Wavedash game can send its achievements in Wavedash's list shape ([{ "identifier", "display_name", "description", "secret" }]), and its getOrCreateLeaderboard makes its boards as it runs.
Put spawn.js beside your index.html (keep its name spawn.js) and load it before your game:
<script src="spawn.js"></script>
<script src="game.js"></script>
For TypeScript, add the types file from the link above to your project. With a bundler, keep the script tag in index.html (do not import spawn.js).
Phaser: report your loading from the Preloader scene, then start. A game with no Preloader can leave them out.
this.load.on('progress', p => Spawn.loading(p));
this.load.once('complete', () => Spawn.loaded());
three.js: manager.onProgress = (url, done, total) => Spawn.loading(done / total) and manager.onLoad = () => Spawn.loaded() on your THREE.LoadingManager.
spawn.gd at res://addons/spawn/spawn.gd.SpawnSDK: Project > Project Settings > Globals > Autoload, or add this to project.godot:[autoload]
SpawnSDK="*res://addons/spawn/spawn.gd"
spawn.js beside the exported index.html and add <script src="spawn.js"></script> in Export > Web > HTML > Head Include. You can leave that line in when you publish: on Spawn, Spawn's copy loads first and yours does nothing. Godot packs every file of the project into the game, so keep the cover and Spawn's JSON files in a folder with an empty .gdignore file (for example spawn/), and pass --attest spawn/spawn-attest.json.Each call is the JavaScript call on SpawnSDK with _ for ., awaited: Spawn.saves.set(name, data) is await SpawnSDK.saves_set(name, data). It returns a Dictionary: { "ok": true, "data": ... }, or { "ok": false, "code": "...", "message": "..." }. Bytes travel as PackedByteArray. Options are the same as in JavaScript, as a Dictionary: await SpawnSDK.leaderboards_submit("distance", score, {"keepBest": false}). SpawnSDK.player is set at the first frame, on Spawn and off it. ready is Godot's own word, so the handshake is await SpawnSDK.begin().
func _ready():
SpawnSDK.purchase.connect(_on_purchase)
var save = await SpawnSDK.saves_get("progress")
if save.ok and save.data != null:
best = save.data.data.best
func game_over():
await SpawnSDK.saves_set("progress", {"best": best})
var posted = await SpawnSDK.leaderboards_submit("distance", score)
await SpawnSDK.achievements_unlock("first-run")
func _on_buy_pressed():
await SpawnSDK.store_buy("gold-runner")
func _on_purchase(purchase):
if purchase.key == "gold-runner":
gold = true
Signals: event(name, data) for every event, player_changed(player) and purchase(purchase). Any call by its path: await SpawnSDK.request("lobby.create", [{"max": 4}]).
Already using Wavedash's Godot plugin? Keep it. Spawn answers it.
SpawnSDK.cs and Spawn.jslib in Assets/Plugins/Spawn/.Each call is awaited on SpawnSDK and answers a SpawnSDK.Answer (Ok, Code, Message, and DataJson, the data as JSON text that you read with JsonUtility or Newtonsoft):
var save = await SpawnSDK.SavesGet("progress");
if (save.Ok && save.DataJson != "null") { /* read save.DataJson */ }
await SpawnSDK.SavesSet("progress", new Dictionary<string, object> { { "best", best } });
await SpawnSDK.LeaderboardsSubmit("distance", score);
await SpawnSDK.AchievementsUnlock("first-run");
await SpawnSDK.StoreBuy("gold-runner");
SpawnSDK.OnEvent += (name, json) => { if (name == "purchase") { /* grant it */ } };
LeaderboardsSubmit(board, value, options) and AchievementsUnlock(keys) take the same options as the JavaScript calls. Any call by its path: await SpawnSDK.Request("lobby.create", new Dictionary<string, object> { { "max", 4 } }). SpawnSDK.PlayerJson is the person playing.
Already using Wavedash's Unity package? Keep it. Spawn answers it.
A game written against Wavedash's SDK runs on Spawn unchanged: Wavedash and WavedashJS are Spawn's, with Wavedash's names, arguments, events and { success, data, message } answers. What each call reaches on Spawn:
getUser() is the person playing. Player ids in Wavedash's calls are Spawn's player ids without their pa_ start.setStat, setAchievement, storeStats) are the player's state and achievements. They load when the game calls init; as on Wavedash, a stat or an achievement set before they have loaded is dropped (the call answers false). An achievement Wavedash unlocks when a stat reaches a threshold unlocks the same way on Spawn: send its stat_requirement with your achievements (Wavedash's list shape) and setStat unlocks it.getOrCreateLeaderboard makes one; a board in seconds reads as a time on Spawn, one in milliseconds or game ticks as a number. Spawn shows no count of a board's players, so totalEntries and getLeaderboardEntryCount read -1.uploadRemoteFile, downloadRemoteFile, ...) are saves named by the file's path inside your game's data folder: user://progress.json in Godot is the save progress.json.triggerPaywall, isEntitled, PurchaseCompleted, fulfillPurchase) is purchases in spoins on Spawn's sheet.Where Spawn's own call does more, use it beside Wavedash's: Spawn.auth.signIn() lets a guest sign in (Wavedash has no guests), and Spawn.saves.set(name, value) saves a value in one call.
To add the SDK to a game: read the game's code, find where it starts, where a run ends, and what it keeps between runs. Then:
spawn.js (JavaScript), add the autoload (Godot) or add the two files (Unity), as above.saves, post scores with leaderboards.submit, unlock achievements, and sell with store.buy and the purchase event.