SEARICHING SDK

Build arcade games that ship with identity, scores, profiles, progression, and saves.

The Portal SDK is the public integration layer for HTML5 games on SEARICHING. Your game talks to window.SeaGames; the platform handles Clerk identity, Supabase-backed leaderboards, progression, cloud saves, profiles, sandbox testing, and origin isolation.

Quickstart

1. Load the SDK
<!-- index.html -->
<script src="https://seariching.com/sdk/portal-sdk.v1.js"></script>
<script type="module" src="./game.js"></script>
2. Submit a score
const sea = await window.SeaGames.init();

const user = await sea.getUser();
console.log(user?.guest ? "Guest player" : `Signed in as ${user?.name}`);

const result = await sea.submitScore("high-score", finalScore, {
  level,
  durationMs,
});

const leaderboard = await sea.getLeaderboard("high-score", { limit: 10 });

SeaGames.init() resolves both on-platform and in local development. On SEARICHING it connects to the parent portal throughpostMessage. Off-platform it returns a standalone shim, so your game loop can use the same code before upload.

Core concepts

Portal first

Uploaded games should use the browser Portal SDK. They should not talk directly to Supabase, Clerk, cookies, or internal app routes.

Boards are modes

Each game.json mode becomes a leaderboard board. Boards return sortDirection: desc for high-score wins and asc for fastest or lowest wins.

Capabilities gate features

Read ready().capabilities before additive features. Current public capabilities are leaderboards, profiles, progression, ranked, saves, and multiplayer.

Sandbox is honest

The test harness validates calls and returns SDK-shaped responses, but score writes are dry-runs and do not pollute production boards.

Guides

Cloud saves
const sea = await window.SeaGames.init();

await sea.save("slot-1", {
  inventory,
  checkpoint,
  settings,
});

const save = await sea.load("slot-1");
Profiles
const sea = await window.SeaGames.init();

const profile = await sea.getProfile();
await sea.updateProfile({ name: "CAPTAIN PIXEL" });

const rival = await sea.getProfileById("user_123");
Match results
const sea = await window.SeaGames.init();

if (sea.capabilities.includes("progression")) {
  const result = await sea.submitMatchResult({
    idempotencyKey: crypto.randomUUID(),
    mode: "league",
    stats: {
      won: true,
      linesCleared: 42,
      quads: 3,
    },
    ranked: {
      won: true,
      opponentTr: 12500,
    },
  });

  console.log(result.xpGained, result.trDelta);
}
Pause and resume
const sea = await window.SeaGames.init();

const unsubscribePause = sea.on("pause", () => {
  game.pause();
});

const unsubscribeResume = sea.on("resume", () => {
  game.resume();
});

Auth behavior

Reads are broadly available. Ranked writes, cloud saves, and profile updates require a signed-in player and reject withUNAUTHENTICATED for guests.

NPM package

Browser games should load the hosted script. Use the npm package for TypeScript contracts, webhook verification, realtime helpers, and AI-agent implementation context.

Install
npm install @seariching/sdk
Typed integration
import type { SeaGamesClient } from "@seariching/sdk/portal";
import { createSeaAnalytics } from "@seariching/sdk/analytics";
import { createSeaEntitlements } from "@seariching/sdk/entitlements";
import { createSeaGameOverrides } from "@seariching/sdk/game-overrides";
import { createSeaPlayerData } from "@seariching/sdk/player-data";
import { createSeaRemoteConfig } from "@seariching/sdk/remote-config";
import { createSeaCloudSaves } from "@seariching/sdk/saves";
import { verifyWebhookSignature } from "@seariching/sdk/webhooks";
import { createRealtime } from "@seariching/sdk/realtime";

declare global {
  interface Window {
    SeaGames: {
      init(options?: { parentOrigin?: string }): Promise<SeaGamesClient>;
    };
  }
}
Source docs for agents
docs/sdk/README.md     # full human + agent guide
docs/sdk/llms.txt      # compact Codex/Claude implementation index
.agents/skills/seariching-sdk/SKILL.md
public/sdk/portal-sdk.d.ts
packages/sdk/README.md
@seariching/sdk/portal

Portal types, request/response protocol constants, and client contracts for game integrations.

@seariching/sdk/webhooks

Webhook signing and verification helpers for trusted server endpoints.

@seariching/sdk/realtime

Room, presence, broadcast, and peer-state helpers for multiplayer prototypes.

@seariching/sdk/saves

Cloud Save v2 slots, atomic versions, and conflict-detected save/delete mutations for trusted hosts.

@seariching/sdk/player-data

Mediated reads, small per-player key/value state, optimistic versions, and owner-authoritative access for trusted hosts.

@seariching/sdk/player-names

Mediated globally unique player display names, public lookup, and availability checks for trusted hosts.

@seariching/sdk/stats

Typed player stats with mediated reads, database-enforced client policies, and owner-authoritative writes.

@seariching/sdk/achievements

Achievement definitions, hidden owner definitions, mediated player reads, progress/unlock policies, and owner-authoritative awards.

@seariching/sdk/admin-debug

Owner-authorized player debug snapshots across profile, saves, player data, stats, achievements, economy, entitlements, presence, and moderation.

@seariching/sdk/audit-logs

Owner-authorized administrative audit event recording and filtered inspection through mediated RPCs.

@seariching/sdk/analytics

Gameplay analytics, crash/error ingestion, and owner telemetry inspection through mediated database RPCs.

@seariching/sdk/chat

Text channels, RPC-mediated channel/message/member reads, direct-message block enforcement, player reports, and owner moderation actions.

@seariching/sdk/cloud-code

Owner-registered HTTP scripts, owner-managed secrets, mediated script/invocation reads, and Worker-backed execution for server-side game logic.

@seariching/sdk/content-delivery

Upload, embed, asset URL helpers, game.json validation, and mediated versioned release metadata for trusted host tools.

@seariching/sdk/economy

Wallets, inventory, mediated reads, idempotent ledger, and owner-authoritative mutation helpers for trusted hosts.

@seariching/sdk/entitlements

Mediated definitions, ownership reads, durable unlocks, subscriptions, idempotent grant/revoke helpers, and owner-authoritative grants.

@seariching/sdk/remote-config

Live config defaults, mediated active reads, segment overrides, and owner publishing/deactivation for trusted hosts.

@seariching/sdk/game-overrides

Scheduled overrides, mediated active reads, segment targeting, percentage rollout bucketing, and owner publishing/deactivation for trusted hosts.

@seariching/sdk/guilds

Guild/clan creation, public discovery, member rosters, invites, open join, and role-gated invite authority through mediated RPCs.

@seariching/sdk/matchmaking

Mediated public lobby listing, public/private lobbies, idempotent room creation, atomic capacity-checked joins, room leaves, and host closes.

@seariching/sdk/server-hosting

Mediated server build, fleet, queued authoritative session, and runner lifecycle helpers for trusted hosts.

@seariching/sdk/social

Friends, unfriend/remove, blocks, game invites, and friend-visible presence through player-scoped RPCs.

@seariching/sdk/leaderboards

Platform-host leaderboard engine with mediated board listing, score submit, leaderboard pages, and my-rank reads. Not the normal path for uploaded games.

Trusted host services

These APIs are for game-owner dashboards, server jobs, launchers, and trusted hosts with an authenticated Supabase client. Uploaded game iframes should keep using the Portal SDK instead of calling these owner APIs directly.

Remote config live tuning
import { createSeaRemoteConfig } from "@seariching/sdk/remote-config";

const remoteConfig = createSeaRemoteConfig({
  supabase,
  gameSlug: "my-rpg",
  getOwnerId: () => owner.id,
});

const xpMultiplier = await remoteConfig.get("xp.multiplier", 1, {
  segments: ["event_weekend"],
});

const published = await remoteConfig.publish("xp.multiplier", 1.5, {
  segment: "event_weekend",
  priority: 20,
});

await remoteConfig.deactivate(published.key, {
  segment: published.segment,
});
Game overrides and rollouts
import { createSeaGameOverrides } from "@seariching/sdk/game-overrides";

const gameOverrides = createSeaGameOverrides({
  supabase,
  gameSlug: "my-rpg",
  getOwnerId: () => owner.id,
  getRolloutId: () => player.id,
});

const spawnTable = await gameOverrides.get("combat.spawn_table", {
  tier: "default",
}, {
  segments: ["ranked"],
});

const published = await gameOverrides.publish("combat.spawn_table", {
  tier: "hard",
}, {
  segment: "ranked",
  rolloutPercentage: 50,
  priority: 20,
});

await gameOverrides.deactivate(published.key, {
  segment: published.segment,
});
Analytics and crash inspection
import { createSeaAnalytics } from "@seariching/sdk/analytics";

const analytics = createSeaAnalytics({
  supabase,
  gameSlug: "my-fps",
  getUserId: () => player.id,
  getOwnerId: () => owner.id,
});

await analytics.track({
  name: "match.completed",
  payload: { won: true, mode: "ranked" },
  sessionId: "session-1",
});

await analytics.reportError({
  name: "render.failed",
  message: "Renderer failed",
  stack: error.stack,
  sessionId: "session-1",
});

const matches = await analytics.listEvents({
  name: "match.completed",
  limit: 50,
});
const crashes = await analytics.listCrashReports({ limit: 25 });
Stats and progression counters
import { createSeaStats } from "@seariching/sdk/stats";

const stats = createSeaStats({
  supabase,
  gameSlug: "my-fps",
  getUserId: () => player.id,
  getOwnerId: () => owner.id,
});

const definitions = await stats.listDefinitions();
await stats.increment("kills", 1);
const kills = await stats.get("kills");
const myStats = await stats.list();

await stats.setForPlayer("player_2", "mmr", 1200);
await stats.incrementForPlayer("player_2", "ranked_wins", 1);
Achievements and awards
import { createSeaAchievements } from "@seariching/sdk/achievements";

const achievements = createSeaAchievements({
  supabase,
  gameSlug: "my-arcade",
  getUserId: () => player.id,
  getOwnerId: () => owner.id,
});

const visible = await achievements.listDefinitions();
const hiddenForOwner = await achievements.listDefinitions({
  includeHidden: true,
});
const unlocked = await achievements.list();

await achievements.progress("clear_100_lines", 4);
await achievements.unlock("first_win");
await achievements.unlockForPlayer("player_2", "secret_boss");
Economy and inventory
import { createSeaEconomy } from "@seariching/sdk/economy";

const economy = createSeaEconomy({
  supabase,
  gameSlug: "my-rpg",
  getUserId: () => player.id,
  getOwnerId: () => owner.id,
});

const currencies = await economy.listCurrencies();
const itemDefinitions = await economy.listItemDefinitions();
const coin = await economy.getBalance("coin");
const inventory = await economy.listInventory();

await economy.grantCurrency("coin", 100, {
  idempotencyKey: "quest-1-reward",
});
await economy.grantItem("iron_sword", 1, {
  idempotencyKey: "quest-1-sword",
});
await economy.spendCurrency("coin", 25, {
  idempotencyKey: "shop-buy-1",
});

await economy.grantCurrencyForPlayer("player_2", "coin", 100, {
  idempotencyKey: "server-quest-player-2",
});
await economy.grantItemForPlayer("player_2", "event_badge", 1, {
  idempotencyKey: "event-badge-player-2",
});
Entitlements and ownership
import { createSeaEntitlements } from "@seariching/sdk/entitlements";

const entitlements = createSeaEntitlements({
  supabase,
  gameSlug: "my-rpg",
  getUserId: () => player.id,
  getOwnerId: () => owner.id,
});

const definitions = await entitlements.listDefinitions();
const ownsBattlePass = await entitlements.has("battle_pass");
const activeUnlocks = await entitlements.list();

await entitlements.grant("premium_skin", {
  idempotencyKey: "purchase-123",
});
await entitlements.revoke("premium_skin", {
  idempotencyKey: "refund-123",
});

const playerPasses = await entitlements.listForPlayer("player_2");
await entitlements.grantForPlayer("player_2", "battle_pass", {
  idempotencyKey: "admin-grant-player-2",
});
Cloud save slots
import { createSeaCloudSaves } from "@seariching/sdk/saves";

const saves = createSeaCloudSaves({
  supabase,
  gameSlug: "my-rpg",
  getUserId: () => player.id,
});

const slots = await saves.list();
const loaded = await saves.load("slot1");

const saved = await saves.save("slot1", {
  hp: 20,
  quest: "cavern",
}, {
  expectedVersion: loaded?.version ?? 0,
});

await saves.delete("slot1", {
  expectedVersion: saved.version,
});

console.log(slots.map((slot) => slot.version));
Player data records
import { createSeaPlayerData } from "@seariching/sdk/player-data";

const playerData = createSeaPlayerData({
  supabase,
  gameSlug: "my-rpg",
  getUserId: () => player.id,
  getOwnerId: () => owner.id,
});

const loadout = await playerData.get("loadout");
const records = await playerData.list();

const updated = await playerData.set("quest.main", {
  step: 3,
}, {
  expectedVersion: loadout?.version ?? 0,
});

const playerQuest = await playerData.getForPlayer("player_2", "quest.main");
await playerData.setForPlayer("player_2", "quest.main", {
  step: 4,
}, {
  expectedVersion: playerQuest?.version ?? 0,
});

await playerData.delete("quest.main", {
  expectedVersion: updated.version,
});
Admin player debugger
import { createSeaAdminDebug } from "@seariching/sdk/admin-debug";

const adminDebug = createSeaAdminDebug({
  supabase,
  gameSlug: "my-mmo",
  getOwnerId: () => owner.id,
});

const snapshot = await adminDebug.getPlayerSnapshot("player_2");

console.log(snapshot.profile?.level);
console.log(snapshot.wallets, snapshot.inventory);
console.log(snapshot.playerData, snapshot.saves);
console.log(snapshot.moderation);
Owner audit trail
import { createSeaAuditLogs } from "@seariching/sdk/audit-logs";

const auditLogs = createSeaAuditLogs({
  supabase,
  gameSlug: "my-mmo",
  getActorId: () => owner.id,
});

await auditLogs.record({
  action: "moderation.ban",
  targetType: "player",
  targetId: "player_2",
  metadata: { reason: "abuse" },
});

const recentModeration = await auditLogs.list({
  action: "moderation.ban",
  actorId: owner.id,
  targetType: "player",
  createdAfter: "2026-07-01T00:00:00Z",
  limit: 50,
});
Matchmaking lobby lifecycle
import { createMatchmaking } from "@seariching/sdk/matchmaking";

const matchmaking = createMatchmaking({
  supabase,
  gameSlug: "my-fps",
});

const room = await matchmaking.createRoom({
  name: "ranked squad",
  maxPlayers: 4,
  visibility: "private",
  idempotencyKey: matchSetupId,
});

const publicRooms = await matchmaking.listRooms();
await matchmaking.joinRoom(room.code);
await matchmaking.leaveRoom(room.code);
await matchmaking.closeRoom(room.code);
Chat moderation queue
import { createSeaChat } from "@seariching/sdk/chat";

const chat = createSeaChat({
  supabase,
  gameSlug: "my-mmo",
  getUserId: () => player.id,
  getOwnerId: () => owner.id,
});

const channel = await chat.createChannel("public", {
  name: "global",
});
await chat.sendMessage(channel.id, "hello world");

await chat.reportPlayer("player_2", "abuse", {
  details: "abusive chat",
});

const reports = await chat.listModerationReports({
  status: "open",
  limit: 50,
});
await chat.updateModerationReportStatus(reports[0].id, "actioned");

if (reports[0].messageId) {
  await chat.deleteMessageAsOwner(reports[0].messageId);
}
Social graph and presence
import { createSeaSocial } from "@seariching/sdk/social";

const social = createSeaSocial({
  supabase,
  gameSlug: "my-mmo",
  getUserId: () => player.id,
});

await social.sendFriendRequest("player_2");
const friends = await social.listFriends();
const requests = await social.listFriendRequests();

await social.sendInvite("player_2", {
  kind: "lobby",
  context: { roomCode: "ABCD12" },
});
const invites = await social.listInvites();

await social.blockPlayer("abusive_player");
const blocks = await social.listBlocks();

await social.updatePresence({
  status: "online",
  activity: "ranked_lobby",
  metadata: { partySize: 3 },
});
const friendPresence = await social.getPresence("player_2");
Guilds and clans
import { createSeaGuilds } from "@seariching/sdk/guilds";

const guilds = createSeaGuilds({
  supabase,
  gameSlug: "my-mmo",
  getUserId: () => player.id,
});

const guild = await guilds.create("sea-raiders", "Sea Raiders", {
  description: "Ranked crew",
  openJoin: true,
  metadata: { region: "na" },
});

const publicGuilds = await guilds.listGuilds();
const roster = await guilds.listMembers(guild.id);

await guilds.sendInvite(guild.id, "player_2");
const invites = await guilds.listInvites();
await guilds.acceptInvite(invites[0].id);
await guilds.joinOpenGuild(guild.id);

Owner authority

Owner write APIs require getOwnerId() to match the signed-in subject. They publish new immutable versions and use deactivation instead of destructive client-side table writes.

AI agent implementation map

Send https://seariching.com/developers#agents to Codex, Claude, or another coding agent when it needs to understand SEARICHING game integration. The generated skill and compact index keep agents aligned with the current SDK contracts.

OpenAPI and MCP

OpenAPI is not the primary contract for uploaded games because game iframe code talks to the Portal SDK through browserpostMessage, not direct HTTP. MCP is the right future layer for authenticated automation: submit games, update games, inspect deploy state, and read platform diagnostics.

For implementation work, start from the desired player-facing capability and use only the public APIs listed here.

Add leaderboards

Load the hosted Portal script in the game, call init once, submit on run end, and render getLeaderboard rows.

init -> submitScore -> getLeaderboard -> getMyRank

Add cloud saves

Persist small JSON-safe state by slot. Treat sandbox/local results as non-remote writes.

ready.capabilities -> save -> load -> clearSave

Add profiles

Read the signed-in profile, update only client-owned name/avatar fields, and use public profiles for rivals.

getProfile -> updateProfile -> getProfileById

Add multiplayer

Gate on ready().capabilities, join one game-scoped room, relay peer events, and publish lightweight shared state.

ready.capabilities -> sea.mp.join -> room.broadcast -> room.publishState

Add pause handling

Pause loops/audio/timers when the portal tab is hidden and resume only after the matching event.

on('pause') -> on('resume')

Verify webhooks

Use the npm helper on trusted server code with the raw request body and signature header.

@seariching/sdk/webhooks: verifyWebhookSignature

Prototype realtime

Use Supabase-backed rooms for presence and broadcast. Do not treat this as authoritative multiplayer.

@seariching/sdk/realtime: createRealtime().joinRoom

API reference

init

SeaGames.init(options?): Promise<SeaGamesClient>

Creates the Portal client. It resolves on-platform and off-platform; local builds receive a standalone shim.

Auth
None
Returns
SeaGamesClient

ready

sea.ready(): Promise<ReadyResult>

Performs the platform handshake and returns SDK version, slug, player, capabilities, and sandbox state.

Auth
None
Returns
ReadyResult

getUser

sea.getUser(): Promise<PortalUser | null>

Returns the current player identity. Guests are explicit through the `guest` flag.

Auth
None
Returns
PortalUser | null

submitScore

sea.submitScore(board, value, metadata?): Promise<SubmitResult>

Submits a finite numeric score through the platform leaderboard RPC. Metadata must serialize to no more than 2 KB.

Auth
Signed-in player
Returns
{ accepted, best, rank, sandbox? }

getLeaderboard

sea.getLeaderboard(board, { limit }?): Promise<LeaderboardEntry[]>

Reads ranked rows for a board through a mediated leaderboard RPC. Board order is defined by the board's `sortDirection`.

Auth
None
Returns
LeaderboardEntry[]

getMyRank

sea.getMyRank(board): Promise<{ rank, value } | null>

Reads the signed-in player's rank on one board through an authenticated player-scoped RPC.

Auth
Signed-in player
Returns
{ rank, value } | null

listBoards

sea.listBoards(): Promise<BoardRow[]>

Lists the game's boards in author-defined order, including `sortDirection`.

Auth
None
Returns
{ key, name, sortDirection }[]

getProfile

sea.getProfile(): Promise<Profile>

Reads the signed-in player's profile and progression fields through a mediated Portal profile RPC. Progression is read-only to games.

Auth
Signed-in player
Returns
Profile

updateProfile

sea.updateProfile({ name, avatarUrl }): Promise<Profile>

Updates client-owned profile fields only through a mediated Portal profile RPC. XP, level, tier, and rank are rejected.

Auth
Signed-in player
Returns
Profile

submitMatchResult

sea.submitMatchResult(result): Promise<MatchResult>

Submits a finished match for platform-computed XP and optional ranked TR. The game sends stats/result evidence; one atomic service-role RPC writes progression and stores the idempotent result.

Auth
Signed-in player
Returns
MatchResult

save/load/clearSave

sea.save(slot, data), sea.load(slot), sea.clearSave(slot)

Stores per-game cloud save data by slot through mediated Cloud Save RPCs. Local/off-platform behavior uses the same interface.

Auth
Signed-in player for cloud writes
Returns
SaveResult | unknown | void

game.json

Put game.json at the root of your uploaded bundle. The host Worker reads it for metadata, entry file, thumbnail, controls, and leaderboard modes.

Manifest example
{
  "schemaVersion": 1,
  "slug": "my-game",
  "title": "My Game",
  "tagline": "One line that sells it on the cabinet.",
  "description": "A longer blurb shown on the play page.",
  "entry": "index.html",
  "thumbnail": "thumb.png",
  "controls": ["keyboard", "touch"],
  "modes": [
    { "id": "high-score", "label": "High Score", "order": "desc" },
    { "id": "sprint", "label": "Fastest Clear", "order": "asc" }
  ]
}

SDK status

beta

Portal browser SDK

The primary public API for uploaded games: identity, leaderboards, profiles, progression, ranked results, saves, multiplayer, pause/resume.

beta

Webhook signature SDK

Server-side helpers for verifying SEARICHING webhook payloads; outbound dispatch target lookup and delivery logging are service-role RPC mediated.

alpha

Realtime SDK

Supabase-backed rooms, presence, and broadcast. Not matchmaking or authoritative game state.

alpha

Host game services SDKs

Trusted host helpers for cloud saves, player data, typed stats, achievements, economy/inventory, entitlements, remote config, game overrides, analytics, and crash reports.

internal

Core leaderboard engine

Platform-host integration layer. Normal games should use the Portal SDK instead.

internal

Public arcade catalog RPCs

Website-facing published game catalog reads through get_public_game and list_public_games; public UI should not query games directly.

internal

Creator game management RPCs

Owner dashboard game listing and publish controls through list_owner_games and set_owner_game_status; account UI should not query or update games directly.

internal

Account profile RPCs

Private account email, display name, and avatar recipe reads/writes through get_account_profile and set_account_profile; account UI should not query profiles directly.

internal

Webhook dispatch RPCs

Edge Function target resolution and delivery logging through list_webhook_dispatch_targets and record_webhook_delivery; dispatch code should not query webhook tables directly.

Errors

UNAUTHENTICATED

The method requires a signed-in player.

Prompt the player to sign in, or keep the action local.
INVALID

The request shape, board, value, slot, or profile patch is invalid.

Validate inputs before calling the SDK and check board ids from listBoards().
PAYLOAD_TOO_LARGE

Metadata or save data exceeds the documented byte limit.

Store smaller payloads and avoid large replay/blob data.
RATE_LIMITED

The platform is throttling the caller.

Back off and avoid submitting repeated scores in a tight loop.
UNSUPPORTED

The host does not support that method or capability.

Gate additive features on ready().capabilities.
INTERNAL

The platform failed while handling a valid request.

Retry later and surface a non-blocking player message.

Testing

  1. Run your game locally with the hosted SDK script included.
  2. Open the sandbox harness and point it at your local game URL.
  3. Verify every SDK call in the live call log.
  4. Confirm score submits are marked sandbox and do not persist.
  5. Zip the same bundle for upload after the harness is clean.

Release checklist

  • Bundle contains index.html, assets, and game.json.
  • Every board id used in code exists in game.json.
  • Scores are submitted as real values; do not negate fastest-time boards.
  • Metadata is small and JSON-serializable.
  • Game loop pauses on Portal pause and resumes on resume.
  • Guest flows are non-blocking and signed-in writes are tested.