# AIWANNA Cloud Save API v1

Cloud saves are private, account-scoped game state. The game exports and restores JSON; the platform supplies identity, cloud storage, revision checks, three previous snapshots, and save-management UI. A game does **not** need its own main menu. Enable cloud saves in its publisher settings before integrating; enabling the switch alone cannot extract game state.

## Recommended SDK integration

Load `/sdk/cloud-saves-v1.js` from your platform origin. It is independent of the leaderboard SDK and can coexist with `/sdk/v1.js`.

```html
<script src="https://www.ai-made.games/sdk/cloud-saves-v1.js"></script>
<script>
// Construct the default state before initialize; do not start gameplay yet.
let state = { checkpoint: 'intro', level: 1, inventory: [], playSeconds: 0 };

async function boot() {
  try {
    await AIWanna.saves.initialize({
      platformOrigin: 'https://www.ai-made.games', // Exact origin from the copied integration prompt.
      gameId: 'YOUR_GAME_UUID',
      schemaVersion: 1,
      gameVersion: '1.0.0',
      exportState: () => structuredClone(state),
      importState: async data => { state = data; await rebuildScene(state); },
      summary: data => `Level ${data.level}`,
    });
    startGameLoop(); // Only after importState has finished or offline play was explicitly chosen.
  } catch (error) {
    showStartupError(error.message); // Incompatible builds must not silently start over.
  }
}

async function checkpointReached(checkpoint) {
  state.checkpoint = checkpoint;
  try { await AIWanna.saves.save(); }
  catch (error) { console.warn('Save needs attention:', error.code); }
}
boot();
</script>
```

`rebuildScene`, `startGameLoop`, and `showStartupError` are game-specific functions. `exportState()` must return a fresh plain JSON object with every value needed to resume. `importState(data)` must rebuild the scene from that snapshot and may return a Promise. It may run again after a player chooses a cloud save; make it safe to replace the whole scene, stop old timers, and reset existing entities. The SDK captures your default export once during initialization so an explicit choice of an empty cloud save can return to a fresh state.

Always pass `platformOrigin` explicitly: game frames can have a no-referrer policy. Referrer fallback accepts only the platform's known production hosts. An explicitly configured HTTPS origin (or HTTP localhost for development) is trusted only after an exact parent-window/origin/request-ID check. Also pass the exact `gameId`; mismatched sessions are rejected.

`initialize()` holds its Promise pending during transient read failures and conflicts. The platform shows retry/offline/choice controls; once the player resolves the condition, the same Promise resolves and the game starts. It rejects invalid configuration, incompatible save formats, invalid sessions, and identity changes. A verified disabled configuration allows local-only play (`unsupported` with `ready:true`), retains the account partition, and makes no cloud requests. Re-enabling cloud saves later requires reconciling locally changed progress. Never catch an initialization error and automatically overwrite with a fresh game.

### SDK methods

| Method | Behavior |
|---|---|
| `initialize(options)` | Connect once; read, migrate, and import progress before allowing play. Repeated calls return the same startup Promise. |
| `save()` | Export a full snapshot, persist it locally, then sync when the baseline is known. Resolves to the local snapshot; cloud status may still need attention. |
| `load()` | Explicitly read and restore progress. Do not call during active gameplay without pausing it. |
| `retry()` | Refresh the session and retry loading/syncing while retaining current local progress. |
| `playOffline()` | Explicitly allow local play after a cloud read fails. An unknown baseline cannot upload until the player later resolves it. |
| `resolveConflict('cloud' \| 'local')` | Apply the player's explicit choice; a subsequent concurrent edit can still require a new choice. |
| `onStatus(callback)` | Subscribe; returns an unsubscribe function. Status contains `status`, `ready`, optional summaries, time, and error/code. |
| `getStatus()` | Read the latest status without subscribing. |
| `destroy()` | Stop timers and listeners when the game is disposed. Reload to initialize a new instance. |

The SDK checks changed state every 30 seconds after startup. Call `save()` at checkpoints, level completion, and meaningful actions; do not rely on page close or mobile background events to save. Unchanged snapshots do not create extra writes. Saving first persists only the SDK's own local record; it never collects unrelated localStorage keys. If browser storage is full or blocked, it reports `LOCAL_WRITE_FAILED` and does not claim the state is saved or upload that new snapshot.

Guests keep local-only progress in the current browser/game origin. Signing in does not automatically transfer it: the player must choose to import the guest snapshot, even if the account has no cloud save. Accounts have separate local partitions. Account switches lock the running SDK until reload, so an old scene cannot save into a new account.

## Snapshot protocol

```json
{
  "protocolVersion": 1,
  "schemaVersion": 2,
  "gameVersion": "1.4.0",
  "summary": "Chapter 4 · Level 12",
  "baseRevision": 17,
  "data": {
    "checkpoint": "chapter-4-camp",
    "player": { "level": 12, "hp": 85, "gold": 320 },
    "inventory": [{ "itemId": "iron-sword", "count": 1 }],
    "completedQuests": ["intro", "forest"],
    "playTimeSeconds": 5420
  }
}
```

- `protocolVersion` is the platform protocol and is currently `1`.
- `schemaVersion` is the game's data format, a positive integer. It must match the configured writable version. `gameVersion` is a nonempty release label of at most 100 characters.
- `summary` is player-friendly text, at most 200 characters. It is metadata, not the source of truth for restoring progress.
- `baseRevision` is the last server revision actually read or acknowledged. Never infer `0` from a failed read. A missing save still has a revision, because deleted/reset saves retain a tombstone.
- `data` is a plain JSON object. Strings, booleans, null, arrays, finite numbers and nested objects are supported. Integer numbers must be JavaScript-safe; encode larger integers as strings. Maximum nesting is 64. Dates should be strings; engine objects should be converted to IDs and ordinary values. Functions, undefined, BigInt, symbols, cycles, sparse arrays and non-plain objects are invalid.
- The complete UTF-8 request must fit within **262,144 bytes (256 KiB)**. The SDK reserves room for `baseRevision` when checking a snapshot.
- Use stable IDs to link objects. Mid-battle saves also need remaining timers, enemies, random-generator state and other deterministic state. Start with checkpoints when an engine cannot reliably restore the middle of a frame.

Only one slot, `main`, is supported in v1. Complete snapshots are stored; this is not a per-frame telemetry or event-log API.

### Migrating game formats

Increment the developer setting and `schemaVersion` in the new build together. Include a migration when old saves exist:

```js
migrateState: (data, fromVersion, toVersion) => {
  if (fromVersion === 1 && toVersion === 2) {
    return { ...data, inventory: data.items || [] };
  }
  throw new Error('This save needs a supported migration path.');
}
```

Migrations receive a copy, must return plain JSON, and must not change the original archive. The SDK validates and imports the migrated snapshot before attempting to save it with the prior cloud revision. Newer unknown formats and older formats without a migration are preserved and block saving. Do not silently reset them. Older backups remain restorable through a build that can migrate their format.

## Native REST integration

No SDK/package is required. A native integration must implement the same startup gate, local persistence, stable retry queue, account partitioning, conflict handling, and management-command acknowledgments as the SDK.

### Obtain a game-scoped session

Register a listener before posting this request to the exact platform origin:

```js
const platformOrigin = 'https://www.ai-made.games';
const requestId = crypto.randomUUID();
window.parent.postMessage({
  source: 'aimadegames', type: 'cloud-save-session', requestId,
}, platformOrigin);
```

Accept the response **only** if `event.source === window.parent`, `event.origin === platformOrigin`, and source/type/requestId match. Successful `apiOrigin` must also equal `platformOrigin`. Remove the listener after success, failure, or a 15-second timeout. Never send to `*` or trust an origin embedded only in message data.

```json
{
  "source": "aimadegames-platform",
  "type": "cloud-save-session",
  "requestId": "YOUR_REQUEST_UUID",
  "apiOrigin": "https://www.ai-made.games",
  "session": {
    "signedIn": true,
    "identity": "opaque-game-account-partition",
    "gameId": "YOUR_GAME_UUID",
    "token": "opaque-save-token",
    "expiresAt": "2026-10-11T12:15:00Z",
    "config": { "enabled": true, "schemaVersion": 1, "maxBytes": 262144, "historyLimit": 3 }
  }
}
```

A guest response has `signedIn:false` and no cloud token. The parent uses its own platform cookies to obtain this session; the game never receives the platform login token. Save tokens are short-lived, scoped to game, account, origin, and save capability, and may be revoked. Leaderboard tokens cannot access saves. Keep credentials in memory; never include them in the serialized state, local queue, logs, or source control.

### Read and write

All URLs below are relative to `platformOrigin`. For game-side requests send `Authorization: Bearer <save token>` and `credentials:'omit'`.

- `GET /api/v1/games/{gameId}/saves`: list metadata only.
- `GET /api/v1/games/{gameId}/saves/main`: returns `{save: <complete snapshot plus revision, updatedAt, sizeBytes, slot> | null, revision}`.
- `PUT /api/v1/games/{gameId}/saves/main`: send the snapshot above with `Content-Type: application/json` and a UUID `Idempotency-Key`. Returns `{save: <metadata only>, revision}`; use your submitted data as the acknowledged local snapshot. The response intentionally omits the state body.

A minimal transport example, **after** a successful read and local persistence:

```js
const body = JSON.stringify({
  protocolVersion: 1, schemaVersion: 1, gameVersion: '1.0.0',
  summary: 'Level 2', data: { level: 2 }, baseRevision: lastRead.revision,
});
const key = crypto.randomUUID(); // Persist this key and the exact body before sending.
const response = await fetch(`${platformOrigin}/api/v1/games/${gameId}/saves/main`, {
  method: 'PUT', credentials: 'omit',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'Idempotency-Key': key },
  body,
});
const result = await response.json();
if (!response.ok) throw Object.assign(new Error(result.error.message), { code: result.error.code });
```

Keep both key and body unchanged across network/401/429/503 retries. If state changes while a write is pending, retain that pending request and queue the latest snapshot for a **new** request only after acknowledgment. Use bounded exponential backoff and respect `Retry-After`. A 401 requires a fresh handshake, including another identity check. A 409 requires an explicit player choice; re-read current cloud state and submit the chosen snapshot using its actual revision and a new key. Never implement last-upload-wins.

### Platform-owned save management

The game's detail page and the platform player offer management. Same-origin authenticated platform UI can read history and restore/reset/delete; game tokens cannot perform those management mutations.

- `GET /api/v1/games/{gameId}/saves/main/history`: previous snapshot metadata.
- `POST .../saves/main/restore`: `{baseRevision, revision}` selects a previous revision.
- `POST .../saves/main/reset`: `{baseRevision}` starts fresh while keeping previous progress as a backup.
- `DELETE .../saves/main`: `{baseRevision}` deletes current progress and backups; the revision tombstone remains.

Cookie-authenticated `GET /saves`, `GET /saves/main`, and `GET /saves/main/history` also return a top-level opaque `identity` for the account that was read. The management UI must retain that identity with the confirmation and send it as `X-Save-Identity` on every cookie-authenticated save mutation, alongside `Idempotency-Key`. Successful cookie mutation responses return the same top-level `identity`. If the current cookies now identify another account, or the identity header is missing or does not match, the server rejects the mutation with `409 IDENTITY_CHANGED`; dismiss the old confirmation and reload account state. A matching revision alone does not authorize a confirmation prepared for another account. Never substitute a freshly read identity into an already confirmed old-account operation.

This extra identity header applies to platform cookie management only. Game requests using their scoped `Authorization: Bearer <save token>` retain the original protocol and do not require `X-Save-Identity`.

Mutations return metadata and the new revision, not state contents. The UI confirms destructive changes, waits for game sync to pause, rechecks the latest revision and identity, performs the operation, and asks the SDK to clear its pending queue. Reload the frame only after a successful `discard-local` acknowledgment. If the cloud mutation succeeded but local cleanup fails or times out, keep sync paused, explain that cloud progress changed but device cleanup is unfinished, and offer to retry cleanup without repeating the mutation or reloading. A lost pause acknowledgment also requires a compensating resume attempt on cancellation/failure. Other devices' stale writes are rejected rather than resurrecting old progress.

## Parent communication

Send status to the exact parent origin using `{source:'aimadegames',type:'cloud-save-status',status,ready,summary?,updatedAt?,localSummary?,cloudSummary?,code?,error?}`. `ready` becomes true only after the startup gate has completed. Statuses are `loading`, `local`, `saving`, `synced`, `offline`, `conflict`, `error`, `incompatible`, `signed-out`, and `unsupported`.

After gameplay starts, background save/session statuses must not pause gameplay, capture input, or open an overlay. The platform uses a brief corner notice. Native integrations must keep write locks separate from their game loop: an identity change stops saving until reload but does not require an automatic gameplay interruption. Background retries must not import checkpoints or rebuild the live scene. A player's explicit cloud choice may replace progress; keeping local progress must retain newer changes made while syncing was blocked.

Listen for `{source:'aimadegames-platform',type:'cloud-save-command',action,requestId?}` only from the verified parent window/origin. Supported actions: `retry`, `use-cloud`, `use-local`, `play-offline`, `refresh-session`, `pause-sync`, `resume-sync`, `discard-local`.

When a requestId is supplied, reply to the exact parent origin with `{source:'aimadegames',type:'cloud-save-command-result',requestId,action,ok,error?}`. `pause-sync` must stop new writes and **wait for in-flight writes/loads to settle before acknowledging**. `discard-local` waits too, removes the current account's pending local record, and prevents further writes until reload. `refresh-session` must stop automatic writes and require reload if identity has changed. Do not reset a game or report success before these operations finish.

## Privacy and failure behavior

Save state belongs to the signed-in player for that game. The publisher UI does not expose other players' snapshots. State is readable by the game code and stored on that device and platform, so never place passwords or secrets in it. This is client-reported progress, **not anti-cheat verification**; an authenticated save does not prove gameplay was legitimate.

Failed reads are not empty saves. Offline new games carry an unknown baseline and need reconciliation before any upload. Unsupported schemas are retained. Guest/account progress is never silently merged. Account change requires reload. If local browser storage is unavailable, report the failure clearly; the player must not be told progress is safe.

Test inside the actual platform player. A standalone browser tab uses a separate local-only guest partition, starts normally, and does not request a cloud session. When a parent handshake has not succeeded, an explicit offline choice uses an isolated `unassigned` partition; it never guesses an account or reads another account's cached progress. A later successful identity handshake requires reload, followed by an explicit import choice. Browsers may isolate or block third-party local storage; test the target browsers and avoid claiming reliable offline saves when local persistence fails.
