Umicat
Features

Save data

Persist per-player saves and per-game shared data — without writing backend code.

If your game needs to remember things between sessions — high scores, unlocked levels, settings, progress — you don't need to set up a backend. Umicat ships a save-data system the agent wires into your game with a few requests.

There are two save surfaces:

  • Per-player saves (saves.*) — each signed-in player has their own data per game. Anonymous players save to localStorage automatically.
  • Per-game shared data (gameData.*) — one shared bucket per game, visible to all players. Public reads, owner-only writes.

When to use which

NeedUse
Player's high score, progress, settingsPer-player saves
Save slots / "load game" UIPer-player saves
Game-wide leaderboard or daily challengePer-game shared data
Asset paths the game references at runtimePer-game shared data
Anything the game owner sets and players only readPer-game shared data

Asking the agent

You don't write any save code yourself. Ask:

When the player dies, save their final score as the player's high score.
When the game starts, load the saved high score and show it in the HUD.

The agent will:

  1. Pick a save key (e.g. 'highScore').
  2. Wire a load on scene start.
  3. Wire a save on the death event.
  4. Update the HUD widget to read the loaded value.

You'll see the agent's tool calls in the activity strip. On reload, the game persists.

Per-player saves — the contract

The agent has access to a saves skill with three operations:

OperationWhat it does
saves.get(key)Read a value for the current player. Returns null if not set.
saves.set(key, value)Write a value for the current player. Any JSON-serializable shape.
saves.delete(key)Remove a value.

Values can be any JSON: numbers, strings, objects, arrays. Reasonable size limit per key: ~64 KB.

Signed in vs anonymous

  • Signed in: saves go to the Umicat backend, keyed by player account + game. Survives device changes — the player's progress travels with their account.
  • Anonymous: the SDK's local transport stores saves in the browser's localStorage. Player keeps progress on this device only. If they later sign in, anonymous saves do not auto-migrate (yet).

Both paths use the same saves.get/set/delete API — the agent writes code that doesn't know or care which transport is active.

Per-game shared data — the contract

For game-wide values readable by all players (or game-internal config the owner sets):

OperationWho can callWhat it does
gameData.get(key)Anyone (public read)Read a value for the game.
gameData.list(prefix?)Anyone (public read)List all keys under an optional prefix.
gameData.set(key, value)Owner only (signed in as project owner)Write a value.
gameData.delete(key)Owner onlyRemove a value.

Common uses:

  • Daily challenge seed. Owner sets gameData.set('dailyChallenge', { seed: '2026-05-24', enemies: [...] }). Every player reads it on start.
  • Public leaderboard. Owner appends gameData.set('leaderboard.top10', [...]) on a cron via an external script (advanced).
  • Game-internal config the agent might tune without rebuilding — difficulty curves, item drop tables, level definitions.

Example flow — high-score persistence

Tell the agent:

Remember the player's all-time high score. Show "High score: X" in the
HUD top-right. When the player dies, compare current score to the high
score and update if it's higher.

Behind the scenes the agent writes something like:

// On scene start
const highScore = (await saves.get('highScore')) ?? 0;
scene.registry.set('highScore', highScore);

// On player death
const finalScore = scene.registry.get('score');
const currentHigh = scene.registry.get('highScore');
if (finalScore > currentHigh) {
  await saves.set('highScore', finalScore);
  scene.registry.set('highScore', finalScore);
}

The HUD's high-score text widget is bound to registry 'highScore', so it updates the moment the registry value changes.

Example flow — settings persistence

Add a settings menu accessible from a gear icon in the HUD. Sliders for
master volume (0-100), music volume (0-100), and a toggle for sound
effects. Persist the settings across sessions.

Agent wires:

const settings = (await saves.get('settings')) ?? {
  masterVolume: 80,
  musicVolume: 60,
  sfxEnabled: true,
};
// ... apply settings ...
// On change in the settings UI
await saves.set('settings', settings);

Anonymous-friendly games

If your game's design assumes players can play without signing up (arcade-style, embedded, one-time experiences), the localStorage fallback gives them a save that works for them — they just lose it if they clear their browser data or switch devices.

If your game's design requires identity (cross-device progress, leaderboards, real names), tell the agent:

Require sign-in to play. Show a "Sign in to play" splash if the player
is anonymous.

The agent reads the user from the SDK handshake and gates accordingly.

What about save slots?

For "Save 1 / Save 2 / Save 3" UI, use namespaced keys:

saves.set('slot1', { progress: ... })
saves.set('slot2', { progress: ... })
saves.set('slot3', { progress: ... })

The agent can wire a save-slot menu that lists keys via a known prefix and lets the player overwrite any slot.

Limits and guarantees

  • Size: each value can be up to ~64 KB; the per-player total is capped at ~1 MB per game. If you need more, talk to support.
  • Rate: 20 RPC/sec per iframe per player.
  • Durability: signed-in saves are stored server-side with daily backups. localStorage saves live on the player's device only.
  • Privacy: per-player saves are visible only to that player; they are NOT visible to the game owner. If you want owner-visible data, build it through gameData.* instead.

Security model

Save calls never pass through the running game code. The game iframe posts an RPC to Umicat's host, which validates the call against the player's session and only then hits the backend. Tokens never reach your game code.

This means you don't have to (and shouldn't) try to authenticate things yourself in game logic — trust the SDK's API surface.

Next steps

On this page