Portal first
Uploaded games should use the browser Portal SDK. They should not talk directly to Supabase, Clerk, cookies, or internal app routes.
SEARICHING SDK
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.
<!-- index.html -->
<script src="https://seariching.com/sdk/portal-sdk.v1.js"></script>
<script type="module" src="./game.js"></script>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.
Uploaded games should use the browser Portal SDK. They should not talk directly to Supabase, Clerk, cookies, or internal app routes.
Each game.json mode becomes a leaderboard board. Boards return sortDirection: desc for high-score wins and asc for fastest or lowest wins.
Read ready().capabilities before additive features. Current public capabilities are leaderboards, profiles, progression, ranked, saves, and multiplayer.
The test harness validates calls and returns SDK-shaped responses, but score writes are dry-runs and do not pollute production boards.
const sea = await window.SeaGames.init();
await sea.save("slot-1", {
inventory,
checkpoint,
settings,
});
const save = await sea.load("slot-1");const sea = await window.SeaGames.init();
const profile = await sea.getProfile();
await sea.updateProfile({ name: "CAPTAIN PIXEL" });
const rival = await sea.getProfileById("user_123");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);
}const sea = await window.SeaGames.init();
const unsubscribePause = sea.on("pause", () => {
game.pause();
});
const unsubscribeResume = sea.on("resume", () => {
game.resume();
});Reads are broadly available. Ranked writes, cloud saves, and profile updates require a signed-in player and reject withUNAUTHENTICATED for guests.
Browser games should load the hosted script. Use the npm package for TypeScript contracts, webhook verification, realtime helpers, and AI-agent implementation context.
npm install @seariching/sdkimport 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>;
};
}
}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/portalPortal types, request/response protocol constants, and client contracts for game integrations.
@seariching/sdk/webhooksWebhook signing and verification helpers for trusted server endpoints.
@seariching/sdk/realtimeRoom, presence, broadcast, and peer-state helpers for multiplayer prototypes.
@seariching/sdk/savesCloud Save v2 slots, atomic versions, and conflict-detected save/delete mutations for trusted hosts.
@seariching/sdk/player-dataMediated reads, small per-player key/value state, optimistic versions, and owner-authoritative access for trusted hosts.
@seariching/sdk/player-namesMediated globally unique player display names, public lookup, and availability checks for trusted hosts.
@seariching/sdk/statsTyped player stats with mediated reads, database-enforced client policies, and owner-authoritative writes.
@seariching/sdk/achievementsAchievement definitions, hidden owner definitions, mediated player reads, progress/unlock policies, and owner-authoritative awards.
@seariching/sdk/admin-debugOwner-authorized player debug snapshots across profile, saves, player data, stats, achievements, economy, entitlements, presence, and moderation.
@seariching/sdk/audit-logsOwner-authorized administrative audit event recording and filtered inspection through mediated RPCs.
@seariching/sdk/analyticsGameplay analytics, crash/error ingestion, and owner telemetry inspection through mediated database RPCs.
@seariching/sdk/chatText channels, RPC-mediated channel/message/member reads, direct-message block enforcement, player reports, and owner moderation actions.
@seariching/sdk/cloud-codeOwner-registered HTTP scripts, owner-managed secrets, mediated script/invocation reads, and Worker-backed execution for server-side game logic.
@seariching/sdk/content-deliveryUpload, embed, asset URL helpers, game.json validation, and mediated versioned release metadata for trusted host tools.
@seariching/sdk/economyWallets, inventory, mediated reads, idempotent ledger, and owner-authoritative mutation helpers for trusted hosts.
@seariching/sdk/entitlementsMediated definitions, ownership reads, durable unlocks, subscriptions, idempotent grant/revoke helpers, and owner-authoritative grants.
@seariching/sdk/remote-configLive config defaults, mediated active reads, segment overrides, and owner publishing/deactivation for trusted hosts.
@seariching/sdk/game-overridesScheduled overrides, mediated active reads, segment targeting, percentage rollout bucketing, and owner publishing/deactivation for trusted hosts.
@seariching/sdk/guildsGuild/clan creation, public discovery, member rosters, invites, open join, and role-gated invite authority through mediated RPCs.
@seariching/sdk/matchmakingMediated public lobby listing, public/private lobbies, idempotent room creation, atomic capacity-checked joins, room leaves, and host closes.
@seariching/sdk/server-hostingMediated server build, fleet, queued authoritative session, and runner lifecycle helpers for trusted hosts.
@seariching/sdk/socialFriends, unfriend/remove, blocks, game invites, and friend-visible presence through player-scoped RPCs.
@seariching/sdk/leaderboardsPlatform-host leaderboard engine with mediated board listing, score submit, leaderboard pages, and my-rank reads. Not the normal path for uploaded games.
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.
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,
});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,
});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 });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);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");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",
});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",
});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));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,
});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);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,
});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);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);
}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");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 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.
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.
Send this URL to Codex or another coding agent when it needs to understand SEARICHING game integration.
https://seariching.com/developers#agentsUse when the agent runtime can install a skill from a URL, or fetch it and copy it into an equivalent SKILL.md.
https://seariching.com/agents/seariching-sdk.mdUse when the agent only supports adding URL/file context. This is generated from the SDK docs.
https://seariching.com/sdk/llms.txtUse as the browser Portal SDK type contract for game iframe code.
https://seariching.com/sdk/portal-sdk.d.tsOpenAPI 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.
Load the hosted Portal script in the game, call init once, submit on run end, and render getLeaderboard rows.
init -> submitScore -> getLeaderboard -> getMyRankPersist small JSON-safe state by slot. Treat sandbox/local results as non-remote writes.
ready.capabilities -> save -> load -> clearSaveRead the signed-in profile, update only client-owned name/avatar fields, and use public profiles for rivals.
getProfile -> updateProfile -> getProfileByIdGate on ready().capabilities, join one game-scoped room, relay peer events, and publish lightweight shared state.
ready.capabilities -> sea.mp.join -> room.broadcast -> room.publishStatePause loops/audio/timers when the portal tab is hidden and resume only after the matching event.
on('pause') -> on('resume')Use the npm helper on trusted server code with the raw request body and signature header.
@seariching/sdk/webhooks: verifyWebhookSignatureUse Supabase-backed rooms for presence and broadcast. Do not treat this as authoritative multiplayer.
@seariching/sdk/realtime: createRealtime().joinRoomSeaGames.init(options?): Promise<SeaGamesClient>Creates the Portal client. It resolves on-platform and off-platform; local builds receive a standalone shim.
sea.ready(): Promise<ReadyResult>Performs the platform handshake and returns SDK version, slug, player, capabilities, and sandbox state.
sea.getUser(): Promise<PortalUser | null>Returns the current player identity. Guests are explicit through the `guest` flag.
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.
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`.
sea.getMyRank(board): Promise<{ rank, value } | null>Reads the signed-in player's rank on one board through an authenticated player-scoped RPC.
sea.listBoards(): Promise<BoardRow[]>Lists the game's boards in author-defined order, including `sortDirection`.
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.
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.
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.
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.
Put game.json at the root of your uploaded bundle. The host Worker reads it for metadata, entry file, thumbnail, controls, and leaderboard modes.
{
"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" }
]
}The primary public API for uploaded games: identity, leaderboards, profiles, progression, ranked results, saves, multiplayer, pause/resume.
Server-side helpers for verifying SEARICHING webhook payloads; outbound dispatch target lookup and delivery logging are service-role RPC mediated.
Supabase-backed rooms, presence, and broadcast. Not matchmaking or authoritative game state.
Trusted host helpers for cloud saves, player data, typed stats, achievements, economy/inventory, entitlements, remote config, game overrides, analytics, and crash reports.
Platform-host integration layer. Normal games should use the Portal SDK instead.
Website-facing published game catalog reads through get_public_game and list_public_games; public UI should not query games directly.
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.
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.
Edge Function target resolution and delivery logging through list_webhook_dispatch_targets and record_webhook_delivery; dispatch code should not query webhook tables directly.
UNAUTHENTICATEDThe method requires a signed-in player.
Prompt the player to sign in, or keep the action local.INVALIDThe request shape, board, value, slot, or profile patch is invalid.
Validate inputs before calling the SDK and check board ids from listBoards().PAYLOAD_TOO_LARGEMetadata or save data exceeds the documented byte limit.
Store smaller payloads and avoid large replay/blob data.RATE_LIMITEDThe platform is throttling the caller.
Back off and avoid submitting repeated scores in a tight loop.UNSUPPORTEDThe host does not support that method or capability.
Gate additive features on ready().capabilities.INTERNALThe platform failed while handling a valid request.
Retry later and surface a non-blocking player message.index.html, assets, and game.json.game.json.pause and resumes on resume.