Sign in with Metaloot
Metaloot OAuth lets browser games authenticate players with their existing Metaloot account. Metaloot still uses Clerk internally; your game only integrates with Metaloot as the identity provider.
Flow
- 1. Publish your game at /publish. You get a client ID, a one-time client secret, and a pre-filled integration prompt — also available later in the game's settings.
- 2. Add a server-side callback URL to the registered redirect URIs.
- 3. Integrate with the @metaloot/auth adapter — paste the settings prompt into your coding agent, or follow the adapter section below.
- 4. Players sign in with their Metaloot account; your game gets a stable player ID, and sign-ins show up in the game's analytics.
The adapter owns the whole OAuth flow for you. The protocol reference further down is only for stacks the adapter does not cover.
Agent Instructions
Published games get this prompt pre-filled with their client ID and redirect URI in their game settings. This generic version is the same prompt with placeholder values — replace them before pasting it into an agent.
Add Sign in with Metaloot to this game using the official @metaloot/auth adapter. Use the adapter — do not hand-roll the OAuth flow.
Metaloot app:
- Client ID: YOUR_METALOOT_CLIENT_ID
- Redirect URI: https://your-game.com/auth/metaloot/callback
- Game name: Your Game
- Docs: https://metaloot.app/docs/auth
Environment:
- Set METALOOT_CLIENT_ID="YOUR_METALOOT_CLIENT_ID"
- Set METALOOT_CLIENT_SECRET to this app's client secret on the server only. Never expose it in browser code.
- Set METALOOT_SESSION_SECRET to a long random server-only value.
- If the game uses a configurable callback URL, set METALOOT_REDIRECT_URI="https://your-game.com/auth/metaloot/callback"
Implementation requirements:
1. Install the latest version of @metaloot/auth using the project's package manager.
2. Wire the server adapter for the detected stack. The adapter must own:
GET /auth/metaloot/start
GET /auth/metaloot/callback
GET /auth/metaloot/session
GET or POST /auth/metaloot/logout
3. Mount the browser sign-in button with mountMetalootAuth from @metaloot/auth/browser.
4. On game boot, call /auth/metaloot/session and treat signedIn:true as the authenticated player.
5. Remove or bypass old guest-login, #mlt launch-token, or vendored Metaloot SDK logic.
6. After login, hide the sign-in button or replace it with the Metaloot player chip.
7. Keep the adapter's default cookie policy unless there is a specific host constraint. On HTTPS it sets SameSite=None; Secure so Metaloot portal iframes can read the game session.
8. Verify locally and in production that /auth/metaloot/session returns signedIn:true after a real Metaloot login.
9. Verify the embedded portal flow too: when a signed-in Metaloot player opens this game from Metaloot, the iframe should bootstrap through /auth/metaloot/start, create the game session cookie, and return without showing another sign-in button.
For a Node/Express/static game, use this shape:
import express from "express";
import { metalootAuth } from "@metaloot/auth/node";
app.use(metalootAuth({
clientId: process.env.METALOOT_CLIENT_ID,
clientSecret: process.env.METALOOT_CLIENT_SECRET,
sessionSecret: process.env.METALOOT_SESSION_SECRET,
redirectUri: process.env.METALOOT_REDIRECT_URI ?? "https://your-game.com/auth/metaloot/callback",
}));
app.use("/vendor/@metaloot/auth", express.static("node_modules/@metaloot/auth/dist"));
Browser mount:
<div id="metaloot-auth"></div>
<script type="module">
import { mountMetalootAuth } from "/vendor/@metaloot/auth/browser.js";
mountMetalootAuth("#metaloot-auth", { gameName: "Your Game" });
</script>
For a Next.js App Router game, create app/auth/metaloot/[...metaloot]/route.ts:
import { createMetalootRouteHandlers } from "@metaloot/auth/next";
export const { GET, POST } = createMetalootRouteHandlers({
clientId: process.env.METALOOT_CLIENT_ID!,
clientSecret: process.env.METALOOT_CLIENT_SECRET!,
sessionSecret: process.env.METALOOT_SESSION_SECRET!,
redirectUri: process.env.METALOOT_REDIRECT_URI ?? "https://your-game.com/auth/metaloot/callback",
});
Then mount @metaloot/auth/browser from a client component.The Adapter
@metaloot/auth is the recommended integration. It owns the OAuth callback, signed game session, session endpoint, logout endpoint, and browser sign-in state so games do not implement OAuth themselves.
npm install @metaloot/auth@latestVersion 0.1.1 and newer keeps localhost development cookies simple, but sets HTTPS auth cookies as SameSite=None; Secure so Metaloot-hosted iframes can establish and read the game session.
Node or Express games can mount the auth routes before serving static files:
import express from "express";
import { metalootAuth } from "@metaloot/auth/node";
app.use(metalootAuth({
clientId: process.env.METALOOT_CLIENT_ID,
clientSecret: process.env.METALOOT_CLIENT_SECRET,
sessionSecret: process.env.METALOOT_SESSION_SECRET,
redirectUri: "https://your-game.com/auth/metaloot/callback",
}));
app.use("/vendor/@metaloot/auth", express.static("node_modules/@metaloot/auth/dist"));Mount the browser control where the game should show sign-in:
<div id="metaloot-auth"></div>
<script type="module">
import { mountMetalootAuth } from "/vendor/@metaloot/auth/browser.js";
mountMetalootAuth("#metaloot-auth", { gameName: "Your Game" });
</script>Protocol Reference
Everything below documents the raw OAuth 2.0 flow behind the adapter. Use it only when your stack cannot run @metaloot/auth — if you use the adapter, you are done and can skip this.
Authorize
GET https://metaloot.app/oauth/authorize
Token
POST https://metaloot.app/api/oauth/token
Userinfo
GET https://metaloot.app/api/oauth/userinfo
Authorization Request
Redirect the player from the game to Metaloot. Use a random state value and verify it in your callback.
https://metaloot.app/oauth/authorize?response_type=code&client_id=METALOOT_CLIENT_ID&redirect_uri=https%3A%2F%2Fyour-game.com%2Fauth%2Fmetaloot%2Fcallback&scope=profile%20email&state=RANDOM_STATEToken Exchange
Exchange the authorization code from your backend only. Keep the client secret out of browser code.
curl -X POST https://metaloot.app/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=METALOOT_CLIENT_ID" \
-d "client_secret=METALOOT_CLIENT_SECRET" \
-d "code=CODE_FROM_CALLBACK" \
-d "redirect_uri=https://your-game.com/auth/metaloot/callback"Successful response:
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "profile email",
"user": {
"id": "user_...",
"email": "player@example.com",
"name": "Player Name",
"image_url": "https://..."
}
}Load the Player
Use the access token to fetch the Metaloot player profile. The sub value is the stable Metaloot player ID for your game.
curl https://metaloot.app/api/oauth/userinfo \
-H "Authorization: Bearer METALOOT_ACCESS_TOKEN"Next.js Callback Example
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const cookieStore = await cookies();
if (!code || state !== cookieStore.get("metaloot_oauth_state")?.value) {
return NextResponse.json({ error: "Invalid Metaloot callback" }, { status: 400 });
}
const response = await fetch("https://metaloot.app/api/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: process.env.METALOOT_CLIENT_ID!,
client_secret: process.env.METALOOT_CLIENT_SECRET!,
code,
redirect_uri: "https://your-game.com/auth/metaloot/callback",
}),
});
const token = await response.json();
if (!response.ok) {
return NextResponse.json(token, { status: 400 });
}
cookieStore.set("metaloot_access_token", token.access_token, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: token.expires_in,
});
return NextResponse.redirect(new URL("/", request.url));
}Legacy: Hosted Widget
Before the adapter existed, games embedded a hosted sign-in widget and implemented the OAuth callback and session handling themselves. New integrations should use the adapter instead — only keep this if your game already has its own callback and session code.
<div id="metaloot-auth"></div>
<script src="https://metaloot.app/metaloot-auth.js"></script>
<script>
MetalootAuth.mount("#metaloot-auth", {
clientId: "YOUR_METALOOT_CLIENT_ID",
redirectUri: "https://your-game.com/auth/metaloot/callback",
gameName: "Your Game",
});
</script>