Authentication
Two layers. API keys identify your app to the wallet; a session identifies a signed-in player. The wallet is the identity provider — it runs every sign-in flow and issues the session. Your backend is the relying party that verifies that session and mints its own app JWT. The SDK does all three sign-in flows for you; this page covers what each one does under the hood and the exact request/response shapes.
API keys
| Key | Prefix | Runs where | Authenticates |
|---|---|---|---|
| Publishable | pk_test_… / pk_live_… | Frontend (shipped in the game client) | The sign-in endpoints below — start a login, read the signed-in wallet |
| Secret | sk_test_… / sk_live_… | Backend only | Server-to-server calls: session verification, moving money |
Each key is bound to an environment — test (devnet) or live (mainnet). The two are fully isolated: a test session can never be verified with a live key, and test funds never touch mainnet. The publishable key is non-secret and re-viewable in the portal; the secret key is shown once — rotate it there if lost.
pk_ can only act for whoever is currently signed in. An sk_ can move money for any user in your app — keep it on your server and in a secret manager.The three sign-in methods
Every method ends in the same session shape. Enable the ones you want per app in the portal (an app’s allowedAuthMethods). In the SDK they all live on useWalletAuth(), and the <WalletLogin/> component renders whichever are enabled:
import { useWalletAuth } from "@iamgame/wallet-sdk";
const {
user, status, // status: "loading" | "anonymous" | "authenticated"
accessToken, // the session token — hand to YOUR backend to verify
connectExternal, // SIWS — pass an external wallet adapter
connectTelegram, // Telegram Mini App — reads window.Telegram.WebApp.initData
requestEmailOtp, // email → sends a 6-digit code
connectEmail, // email + code → session
logout,
} = useWalletAuth();The rest of this section is the raw HTTP each flow performs. You don’t call these yourself when using the SDK — they’re here so you know what’s happening and can debug it. Every request carries your pk_ as a bearer token.
a. SIWS — external wallet (Phantom, Solflare, Backpack)
A two-step challenge/response. The client asks for a challenge, the player signs it with their browser wallet, and the client sends the signature back. In the SDK this is one call — connectExternal(phantomAdapter()).
Step 1 — request a challenge for the player’s public key:
curl -X POST https://api-wallet.iamgame.com/v1/auth/siws/challenge \
-H "authorization: Bearer $IAMGAME_WALLET_PK" \
-H "content-type: application/json" \
-d '{ "publicKey": "8x…player base58 pubkey" }'{
"nonce": "a1b2c3…",
"domain": "yourgame.com",
"statement": "Sign in to IAMGame Wallet",
"uri": "https://yourgame.com",
"issuedAt": "2026-07-13T10:00:00.000Z",
"expiresAt": "2026-07-13T10:05:00.000Z"
}Step 2 — the player signs the challenge message; POST the signature back to /auth/siws/verify:
{
"challenge": { "nonce": "a1b2c3…", "domain": "…", "statement": "…", "uri": "…", "issuedAt": "…", "expiresAt": "…" },
"publicKey": "8x…player base58 pubkey",
"signature": "base64 ed25519 signature over the challenge",
"referralCode": "OPTIONAL-first-touch-code"
}The response is a session (same shape for all three methods) — see below.
b. Telegram Mini App — auto-login from launch data
Inside a Telegram Mini App the player is already authenticated by Telegram. The SDK reads window.Telegram.WebApp.initData and posts it to /auth/telegram/verify; the signer validates the HMAC with your bot token. With the provider default this happens automatically on mount — zero taps. Manual trigger: connectTelegram().
curl -X POST https://api-wallet.iamgame.com/v1/auth/telegram/verify \
-H "authorization: Bearer $IAMGAME_WALLET_PK" \
-H "content-type: application/json" \
-d '{ "initData": "query_id=…&user=…&hash=…" }'The signer captures the Telegram profile (username, firstName, lastName, photoUrl) against the identity, refreshed on every login. A startapp deep-link param is honoured as a referral code when no explicit referralCode is passed (t.me links can’t carry ?ref=).
c. Email + 6-digit OTP
No wallet or extension needed. Two steps: initiate emails a single-use, short-lived, attempt-capped code; verify exchanges it for a session. In the SDK: requestEmailOtp(email) then connectEmail(email, code).
await requestEmailOtp("player@email.com"); // POST /auth/email/initiate
await connectEmail("player@email.com", "123456"); // POST /auth/email/verify → sessionStep 1 — POST /auth/email/initiate body and response:
// request
{ "email": "player@email.com" }
// response
{ "sent": true, "expiresAt": "2026-07-13T10:05:00.000Z" }Step 2 — POST /auth/email/verify:
{
"email": "player@email.com",
"code": "123456",
"referralCode": "OPTIONAL-first-touch-code"
}/email/initiate and /verify fail with auth/method_not_allowed unless email is in the app’s allowedAuthMethods.The session
All three verify endpoints return the same session object: a short-lived access token plus a longer-lived refresh token and the canonical user. The SDK stores both and refreshes automatically — you only touch accessToken, which you forward to your backend.
{
"accessToken": "eyJ…", // JWT, ~1h. Bearer for the wallet API + your backend to verify.
"refreshToken": "…", // ~7d. Used to mint a fresh access token.
"expiresAt": "2026-07-13T11:00:00.000Z",
"user": {
"id": "usr_…",
"appId": "app_…",
"primaryIdentity": { "id": "…", "type": "email", "externalId": "player@email.com", "profile": { "email": "player@email.com" } },
"identities": [ { "id": "…", "type": "email", "externalId": "player@email.com", "profile": { "email": "player@email.com" } } ],
"wallets": [ { "address": "8x…", "environment": "test", "custody": "operator", "status": "active" } ],
"createdAt": "2026-07-13T10:00:00.000Z"
}
}Refresh & logout
The SDK refreshes for you. Under the hood it posts the refresh token and gets a fresh session:
# refresh — no key required; the refresh token is the credential
curl -X POST https://api-wallet.iamgame.com/v1/auth/refresh \
-H "content-type: application/json" \
-d '{ "refreshToken": "…" }'
# logout — revokes the refresh token; returns 204
curl -X POST https://api-wallet.iamgame.com/v1/auth/logout \
-H "content-type: application/json" \
-d '{ "refreshToken": "…" }'Identity & captured profile
A user is keyed by how they authenticate — an identity is a (type, externalId) pair: a Solana pubkey (SIWS), a Telegram user id, or an email address. The same person signing in the same way always resolves to the same user and the same wallet, portable across your shared-scope apps. Each login also captures a small profile, returned on the session user and on verifySession as identities[].profile:
- SIWS — no profile (the pubkey is the identity).
- Telegram →
{ username, firstName, lastName, photoUrl } - Email →
{ email }
Use it to greet a player by their Telegram @username or gate staff tooling by an email domain — always from the verified claim, never from the browser. More on shared vs isolated wallets in Core concepts.
Server-to-server verification
The important part: never trust a wallet address sent from the browser. Your backend confirms the session with the wallet using your secret key, gets back the canonical identity, and only then mints its own app session.
import { IAMGameWalletServer } from "@iamgame/wallet-sdk-server";
const wallet = new IAMGameWalletServer({
secretKey: process.env.IAMGAME_WALLET_SECRET_KEY!,
baseUrl: "https://api-wallet.iamgame.com/v1",
});
// Your login endpoint receives { sessionToken } (the SDK's accessToken) from the frontend.
const verified = await wallet.verifySession(sessionToken);
// verified = { userId, appId, environment, authMethod, walletAddress, wallets[], identities[] }
// Optional: confirm a specific claimed address really belongs to this session.
if (claimedAddress && verified.walletAddress !== claimedAddress &&
!verified.wallets.some((w) => w.status === "active" && w.address === claimedAddress)) {
throw new Error("wallet does not match the verified session");
}
return signMyAppJwt({ userId: verified.userId, wallet: verified.walletAddress });verifySession is scoped to your key’s app and environment and fails closed — an invalid, expired, or out-of-scope session is rejected. Under the hood it calls POST /v1/sessions/verify with your sk_; call that endpoint directly if you’re not on Node:
curl -X POST https://api-wallet.iamgame.com/v1/sessions/verify \
-H "authorization: Bearer $IAMGAME_WALLET_SECRET_KEY" \
-H "content-type: application/json" \
-d '{ "sessionToken": "…" }'Full backend wiring — client construction, error handling, and calling the wallet — is in Server SDK setup. For building your own login UI on the hook instead of <WalletLogin/>, see Login & session. If you run a referral program, the code a player arrived with is attached to this login — see Referrals in your UI.