Pentagon Identity API

Authentication, user profiles, wallet binding, NFT data, and social integrations for Pentagon Games ecosystem.

🚫 DO NOT redirect or popup to pentagon.games/sign-in or login.pentagon.games.
Build your login form directly on your page and call the Pentagon Identity API from it. No redirects. No browser popups to external sites. Your app shows the form, your app calls POST /user/login, your app gets the JWT. The user never leaves your page.

4 sign-in methods, all via the same POST /user/login API:
1. Email + Password
2. PNS Username + Password
3. Wallet Signature (MetaMask/Rabby/Phantom)
4. Magic Link (passwordless email)

See Integration Patterns for drop-in React & vanilla JS code, or the pentagon-login-widget repo for a standalone reference implementation.
v2.0 — Live App Keys Active
⚠️ Migration Deadline: May 28, 2026 — All login/signup requests must include an X-PG-App-Key header. Currently in soft mode (warnings only). See migration guide →

🚀 Getting Started

Before you can use the Pentagon Identity API, you must have an App Key. Requests without a valid key will be rejected starting May 28, 2026.

Step 1: Request an App Key

Contact the Pentagon Games team to get your key:

Step 2: Add the Header

Include your App Key in all login and signup requests:

X-PG-App-Key: pk_live_your_key

Step 3: Integrate

Use the endpoint reference below to integrate authentication, wallets, NFTs, and social connections into your app.

Current status: App Key enforcement is in soft mode (warnings only in server logs). Full enforcement begins May 28, 2026, after which requests without a valid key will receive HTTP 401. See migration timeline →
🚫 NEVER redirect or open a popup/new window to pentagon.games or login.pentagon.games. This is the #1 integration mistake. Those pages are for pentagon.games itself, not for your app.

✅ Correct approach: Put the login form directly on your page. An inline form, a modal overlay in your DOM, or a dedicated login page on YOUR domain. Your form collects email/password (or wallet signature), calls POST /user/login with your X-PG-App-Key, and gets a JWT back. The user never leaves your page, no new windows open, no redirects happen.

This is how pentagon.games, gunnies.io, vip.pentagon.games, and mining.pentagon.games all work. Each site has its own login form that calls the API directly.

Reference code: github.com/blockchainsuperheroes/pentagon-login-widget — drop-in React component, vanilla JS, and API-only examples.

Overview

The Pentagon Identity API provides unified authentication and user management for all Pentagon Games products. One account works across pentagon.games, Gunnies, EtherFantasy, PentaSwap, and all partner apps.

Infrastructure: This system runs on pg_identity_db, a dedicated PostgreSQL identity database. Fully migrated from the legacy chainguardians database in May 2026. Currently serving 729,291+ registered users across the Pentagon ecosystem.

🔐 Authentication

Email/password, wallet signature, magic link, Ethermail SSO, social OAuth (Discord, Twitter, Telegram, LinkedIn)

👛 Multi-Chain Wallets

EVM (MetaMask, Rabby, Phantom), MultiversX, Tron, TON, Algorand wallet binding

🖼️ NFT Data

Cross-chain NFT ownership, metadata, collection info. Synced by backend oracle from on-chain data.

⭐ VIP & Referrals

Referral codes, VIP status, Discord role sync, friends system

Base URL

https://api.account.pentagon.games

All endpoints are prefixed with /user/ unless otherwise noted.

Authentication Pattern

Most endpoints require a JWT Bearer token obtained from login:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Authentication

Frontend Routes

Pentagon Games Frontend Routes:
  • https://pentagon.games/login — Main user login page
  • https://pentagon.games/sign-in — Redirects to /login (backward compatibility)
  • https://pentagon.games/sso-login-widget — OAuth2 SSO integration endpoint (for external apps)
For your app: Build your own login form and call the API directly (see Integration Patterns), or use the SSO redirect flow via /sso-login-widget for OAuth2 integration.

Login Methods

MethodEndpointDescription
POST /user/login Email/password or wallet signature login
POST /user/login/email Magic link login (sends email)
POST /user/login/ethermail Ethermail SSO login
POST /user/token/refresh Refresh expired JWT token

Email/Password Login

POST /user/login
Content-Type: application/json
X-PG-App-Key: pk_live_your_key

{
  "type": "email",
  "username": "player@example.com",
  "password": "securepassword",
  "login_from": "your_app_name"
}

Wallet Signature Login

For web3 wallet login, the client signs a message and the backend verifies the signature matches the wallet address.

POST /user/login
Content-Type: application/json
X-PG-App-Key: pk_live_your_key

{
  "type": "wallet",
  "address": "0x1234...abcd",
  "signature": "0xsigned...",
  "message": "Logging into Pentagon Games,1745812345",
  "login_from": "your_app_name"
}

Message format: Logging into Pentagon Games,{unix_timestamp_seconds}

Timestamp window: Must be within 5 minutes of server time. The backend does not require a particular message prefix — it requires that the signature recovers to address and that the timestamp after the last comma is under 5 minutes old.

Prerequisite: The address must already be registered on an account — either as mm_address (via /user/bind_metamask) or as penai_address (via /user/penai/bind).

Wallet login now matches mm_address OR penai_address. A user's device-held Pentagon AI (PGAI) wallet is a full login credential — this is what powers silent re-login after unlock in the Pentagon AI clients. The Pentagon AI apps reuse the bind message Sign up to Pentagon Games,{unix_timestamp_seconds} verbatim. See PGAI Wallet.
type accepts only "email" and "wallet". There is no "username", "pns" or "penai" type — "email" runs the canonical resolution chain (email → PNS → username → legacy). Any other value is rejected with {"status": false, "message": "Validation error", "erorlist": {"type": ["This field is required."]}}.

Successful Login Response

{
  "status": true,
  "result": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
  }
}

PNS (Pentagon Name Service) Login

Users with a PNS username NFT can log in using their on-chain name instead of their email or PG username. The backend resolves the PNS name to a wallet address via the on-chain contract, then matches it to the PG account with that wallet bound.

POST /user/login
Content-Type: application/json
X-PG-App-Key: pk_live_your_key

{
  "type": "email",
  "username": "king",
  "password": "userpassword",
  "login_from": "your_app_name"
}

Resolution flow: PNS name → on-chain contract → wallet address → PG account lookup → password verification → JWT token

Contract: 0xf97EB9f8293D1FD5587a809Eb74518c300738d07 on Pentagon Chain (3344)

Requirements: The PNS name must be minted AND spatially bound to a wallet that is connected to a PG account.

Note: PNS names are lowercase only on-chain. When submitting a PNS name for login, use the lowercase form (e.g. king, not King). The backend normalizes input to lowercase before contract lookup.
PNS is now the primary username system. Legacy PG usernames (e.g. "nftprof1") still work for login but are being phased out. Once a user has a bound PNS name, that becomes their identity across all Pentagon services.

Login Methods Summary

MethodExampleStatus
Email + Passwordnftprof@pentagon.gamesActive
PNS Name + Passwordnftprof (on-chain bound)Active
Current Username + Passwordnftprof1Active
Wallet SignatureMetaMask / Rabby / PhantomActive
Magic Link (email)Sends login link to emailActive
Ethermail SSOEthermail tokenActive
Legacy Username + Passwordnftprof (as legacy of nftprof1)Removed
⚠️ Legacy username login removed (April 29, 2026) — Users can no longer log in with legacy/old usernames. Use your current username, email, or PNS name instead. Legacy usernames are retained in the database for PNS NFT airdrop mapping only.

Error Responses

ErrorMeaning
Unauthenticated userWrong credentials or wallet not registered
User not verifiedEmail not validated yet
Invalid SignatureWallet signature verification failed or timestamp expired

🧭 Resolving Users (Handle → Account)

"Resolution" means turning a handle a user typed (or that another service passes in) into a concrete account. The identity backend supports several handle types, and they must always be tried in the same canonical order. Getting this wrong is the single most common identity bug: a partial resolver matches current username but misses PNS or legacy, and silently fails to find a real user.

⚠️ There is ONE canonical resolution order. Use it everywhere.
  1. Email — exact match on user.email (case-insensitive)
  2. PNS name (primary) — lowercase on-chain Pentagon Name Service name → resolve to wallet → find the account whose bound mm_address matches
  3. Current username — exact match on user.username (case-insensitive)
  4. Legacy username — mapping lookup in user_legacy_username (airdrop/migration mapping only — see note)

Why the order matters: the same string can be valid at multiple layers (e.g. nftprof is a PNS name AND a legacy username of nftprof1). PNS is the primary identity system, so it must win before legacy. Email is checked first because it is unambiguous and cheapest.

Internal (in-process) vs. Login resolution

There are two contexts where resolution happens, and they share the same order but differ in scope:

ContextUsed byScope
Login resolutionPOST /user/loginEmail + PNS + current username. Legacy username login was removed (Apr 29, 2026) — it is NOT accepted as a login credential.
Internal resolutionService-to-service / in-process lookups (e.g. the NPC card-spend handler turning a card owner handle into an account)Full chain INCLUDING legacy mapping, because internal lookups ("who owns this card?") legitimately need to resolve old handles that can no longer log in.
Internal callers must use the shared function, not the HTTP API. Anything running inside pentagon-login-backend has direct ORM access. Calling the backend's own REST endpoint to resolve a user is a localhost self-round-trip — slower, and it can deadlock the worker pool under load. Use the in-process resolver resolve_account(handle) (in user/pns.py). The public HTTP endpoint is for external consumers only.

One canonical resolver

All paths — login, the card-spend handler, and any public lookup endpoint — must call a single shared resolve_account(handle) function. Three separate copies of "resolve a handle" previously existed and drifted apart (login did the full chain; info_by_username/public did username + legacy only, missing PNS; a third was added ad hoc). Duplicated resolvers will diverge. Centralize one, reuse it everywhere.

# Canonical internal resolver (user/pns.py) — call this, do not reimplement
account = resolve_account(handle)   # tries email → PNS → username → legacy
if account is None:
    # handle did not resolve at ANY layer — fail loudly, do not assume username-only
    ...
⚠️ Known gap: GET /user/info_by_username/public/<username> currently resolves username + legacy only — it does NOT include the PNS layer. Do not treat it as a full resolver. If you need PNS resolution from an external context, this endpoint is insufficient; either it must be migrated onto resolve_account(), or you must treat it explicitly as username/legacy-only. Internal callers should bypass it entirely and use the shared function.

Normalization rules

Net: document and centralize one resolver (email → PNS → username → legacy), reuse it in login, the spend handler, and any lookup endpoint, and tell internal services to call the function, not the HTTP API. This prevents the silent-resolution-failure class of bug for every future service that touches identity.

🤖 PGAI Wallet (Pentagon AI)

A Pentagon account can carry three different addresses, and integrators keep confusing them. They have different custody models and are not interchangeable. Read this table before you touch any wallet field.

⚠️ Three addresses, three owners. "The user's wallet" is not one thing.
FieldUser-facing nameCustodyRotatable?Chain
penai_address Pentagon AI wallet (PGAI) Self-custodial — key generated and held on the user's device. Pentagon never has it. Yes (capped) Pentagon Chain (3344)
mm_address Connected wallet User's own external wallet (MetaMask, Rabby, Phantom, WalletConnect). Watch-only from our apps — we read it, we never sign with it. No — one-time, irreversible Wherever the user's wallet lives
aa_wallet_address "PG Balance" Custodial — created at email signup. Pentagon holds the key server-side and signs on the user's behalf. n/a Pentagon Chain (3344)
⚠️ aa_wallet_address is NOT ERC-4337 account abstraction. Despite the aa_ prefix it is a plain custodial EOA — a standard keypair created at signup, with the encrypted private key stored by the identity backend. No bundler, no EntryPoint, no UserOperation, no smart-contract wallet anywhere in this path.

The name is retained purely for wire compatibility: it is the join key the EtherFantasy DNA service and other downstream consumers already index on. Do not infer 4337 semantics from the field name. GET /user/walletinfo returns the same value under both wallet_address and aa_wallet_address.

Which one do I use?

Binding & rotating the PGAI address

MethodEndpointAuthDescription
POST/user/penai/bindBearerBind or rotate the caller's PGAI address
GET/user/penai/historyBearerEvery PGAI address this account has held + remaining rotations
POST /user/penai/bind
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "address":    "0xabc...",
  "signature":  "0x...",
  "message":    "Sign up to Pentagon Games,1753400000",
  "login_from": "pentagon-ai-extension"
}

Message format: Sign up to Pentagon Games,{unix_timestamp_seconds} — the same personal_sign (EIP-191) scheme as /user/bind_metamask.

Timestamp window: the signature is rejected once the trailing timestamp is more than 5 minutes old. Sign at call time; never cache a signed message.

login_from: optional, recorded in the address history so a client can later tell the user which app set up their account.

Success response:

{
  "status": true,
  "result": { "penai_address": "0xabc..." }
}
Rotatable — unlike mm_address. /user/bind_metamask is a one-time irreversible bind: a second call with a different address is refused. /user/penai/bind replaces the registered address, and the previous one is retained in the history with released_at stamped — it is never silently lost.
Uniqueness is enforced. A PGAI address may belong to exactly one PG account. Binding an address that is already registered to a different account is refused with {"status": false, "message": "This wallet is already linked to another Pentagon account."}. A unique DB constraint backs the up-front check, so two simultaneous binds still fail safely rather than moving the address.

Rotation limits

CapValueCounts
Lifetime5 distinct addressesEvery distinct address the account has ever held
Rolling window3 binds per 30 daysHistory rows created in the last 30 days (sliding, not calendar)

Both caps are checked before anything is written — a refused rotation leaves the current address untouched. Hitting either returns {"status": false, "message": "You have changed your Pentagon AI address too many times. Contact support."}

Re-binding the SAME address is a free no-op. If address already equals the account's current penai_address, the call returns success, writes no history row, and does not consume a rotation. Clients may call bind idempotently on every unlock without burning the user's quota.

GET /user/penai/history

{
  "status": true,
  "result": {
    "current": "0xnew...",
    "addresses": [
      { "address": "0xnew...", "bound_at": "2026-08-01T09:12:44.318Z", "released_at": null,                       "login_from": "pentagon-ai-telegram"  },
      { "address": "0xold...", "bound_at": "2026-06-14T20:03:01.552Z", "released_at": "2026-08-01T09:12:44.318Z", "login_from": "pentagon-ai-extension" }
    ],
    "rotations_used": 2,
    "rotations_remaining": 3
  }
}
FieldMeaning
currentThe address currently bound. "" if the user has never bound one.
addressesNewest first. released_at: null marks the row for current; every other row carries the moment it was replaced.
login_fromWhich app performed that bind — "" when the client sent none.
rotations_usedCount of distinct addresses ever held (matches the lifetime cap).
rotations_remaining5 - rotations_used, floored at 0.

PGAI as a login credential

POST /user/login with type: "wallet" matches the signed address against either mm_address or penai_address. A device-held PGAI key is therefore a full login credential — this is what powers silent re-login after unlock in the Pentagon AI clients. See Authentication.

login_from values

Sent on POST /user/login, POST /user/bind_metamask and POST /user/penai/bind. On the PGAI bind it is persisted into the address history.

ValueClient
pentagon-ai-telegramPentagon AI on Telegram
pentagon-ai-extensionPentagon AI browser extension
pentagon-ai-mobilePentagon AI mobile app
pentagon-aiGeneric fallback when a host app sets nothing

It is client-supplied and unvalidated — treat it as a display hint, never as an authorisation signal. Third-party integrators should send their own app name.

Cross-device wallet sync

Moving a self-custodial PGAI wallet from a device that has it (source) to a device that wants it (target), without the seed ever reaching Pentagon.

🔒 The server is a DUMB RELAY. It stores only an ephemeral public key and an opaque ciphertext blob it cannot decrypt. Both devices must be authenticated as the same PG account — every view scopes its queryset to the calling user, so another account's channel id simply resolves to "Channel not found". Channels have a 10-minute TTL and the ciphertext is single-read: sync/fetch hands it over exactly once, wipes it, and marks the channel delivered.
MethodEndpointAuthDescription
POST/user/sync/openBearerTarget opens a channel with its ephemeral public key
GET/user/sync/pendingBearerSource lists its own pending requests (never returns ciphertext)
POST/user/sync/approveBearerSource posts the sealed blob
POST/user/sync/rejectBearerSource declines (terminal)
POST/user/sync/fetchBearerTarget polls, then reads the blob once
POST /user/sync/open        { "target_pubkey": "<base64>", "target_label": "Chrome on Windows", "safety_code": "0042" }
        →                    { "status": true, "result": { "channel_id": "...", "expires_in": 600 } }

GET  /user/sync/pending
        →                    { "status": true, "result": [ { "channel_id": "...", "target_pubkey": "<base64>",
                                                            "target_label": "Chrome on Windows", "safety_code": "0042",
                                                            "created_at": "2026-08-05T11:20:03.114Z" } ] }

POST /user/sync/approve     { "channel_id": "...", "ciphertext": "<opaque base64>" }   → { "status": true }
POST /user/sync/reject      { "channel_id": "..." }                                  → { "status": true }

POST /user/sync/fetch       { "channel_id": "..." }
        waiting →            { "status": true,  "result": { "state": "pending" } }
        approved →           { "status": true,  "result": { "state": "approved", "ciphertext": "<base64>" } }
        second fetch →       { "status": false, "message": "Already delivered" }
        rejected →           { "status": false, "message": "Request was rejected on the other device" }

target_pubkey is required (≤200 chars); target_label (≤120) and safety_code (≤12) are optional and exist only so the source device can render a useful prompt. Channel ids are generated server-side — a caller cannot choose or guess one. Opening a channel supersedes the user's other pending channels, so the source only ever sees the latest request.

Channel states: pendingapproveddelivered, with rejected and expired as terminal side-exits.

The ciphertext is opaque to the backend — the encryption scheme is entirely a client concern. The reference Pentagon AI clients seal with ephemeral X25519 key agreement → HKDF-SHA256 (bound to both public keys) → XChaCha20-Poly1305, and show a 4-digit safety code derived from the public keys so the user can number-match both devices before approving.

Never send a seed, mnemonic or private key to any Pentagon endpoint. The relay carries ciphertext only. Keep the target's ephemeral secret key in memory for the life of the attempt — never persist it, never log it.
Full guide: request/response shapes, rotation semantics, error tables and a client checklist live in the PGAI wallet integration guide →
Error convention: application-level failures on these endpoints come back as HTTP 200 with "status": false. Branch on status, not on the HTTP code. Only a missing or invalid JWT produces 401 ({"detail": "Authentication credentials were not provided."} / {"detail": "Token is invalid"}).

API Endpoints

User Info

MethodEndpointAuthDescription
GET/user/infoBearerCurrent user's full profile. Now includes penai_address (the PGAI wallet — empty until bound) alongside mm_address. See PGAI Wallet.
GET/user/info/detailBearerLimited user info
GET/user/info/privateBearerPrivate user info
GET/user/info_by_username/public/<username>NonePublic profile lookup. ⚠️ Resolves username + legacy only — NOT PNS. See User Resolution; do not use as a full resolver.
GET/user/check_usernameAPI KeyCheck username availability
POST/user/edit_detailsBearerUpdate profile

Wallet Binding

MethodEndpointAuthDescription
POST/user/bind_metamaskBearerConnect EVM wallet → mm_address. One-time and irreversible.
POST/user/penai/bindBearerBind or rotate the PGAI wallet → penai_address. See PGAI Wallet.
GET/user/penai/historyBearerEvery PGAI address this account has held + remaining rotations
POST/user/bind_multiversxBearerConnect MultiversX wallet
POST/user/bind_tronBearerConnect Tron wallet
POST/user/bind_tonBearerConnect TON wallet
POST/user/updateAlgorandBearerConnect Algorand wallet
GET/user/walletinfoBearerCustodial platform account ("PG Balance") — returns aa_wallet_address (same value as wallet_address), live PC balance, and NPC points. Despite the name this is a custodial EOA, not ERC-4337 — see PGAI Wallet.
POST/user/wallet/card-spendBearerSpend NPC from the user's internal wallet (backend decrypts stored key + signs CardSpendSplitter)
Internal wallet = identity-owned. The internal PG wallet (AA / NPC wallet) is read and spent only through these identity endpoints with the standard login JWT. Never fetch it from mining (api.mine.pentagon.games) and never derive it from a seed. See Pattern 3: PG Login Only for the full read + spend flow.

Cross-Device Wallet Sync

MethodEndpointAuthDescription
POST/user/sync/openBearerTarget device opens a channel with its ephemeral public key
GET/user/sync/pendingBearerSource device lists its own pending requests (never returns ciphertext)
POST/user/sync/approveBearerSource device posts the sealed blob
POST/user/sync/rejectBearerSource device declines (terminal)
POST/user/sync/fetchBearerTarget device polls, then reads the blob once
🔒 Dumb relay. The server stores only an ephemeral public key and a ciphertext it cannot decrypt. Both devices must be authenticated as the same PG account, channels expire after 10 minutes, and the ciphertext is single-read. Full request/response shapes and the client checklist: PGAI Wallet / integration guide.

NFT Data

MethodEndpointAuthDescription
GET/user/nftsBearerUser's NFTs across all chains
GET/user/nfts?collection=GunniesBearerFilter by collection name
GET/user/nfts?order_by=recently_receivedBearerSort by recent
GET/user/penxr/nftsBearerPenXR NFT display data

Social Auth & Connections

MethodEndpointAuthDescription
POST/user/auth/discordBearerConnect Discord
POST/user/auth/twitterBearerConnect Twitter/X
POST/user/auth/telegramBearerConnect Telegram
POST/user/auth/linkedinBearerConnect LinkedIn
POST/user/social/oauth/<platform>/connectBearerGeneric OAuth connect
POST/user/social/<platform>/disconnectBearerDisconnect platform
PUT/user/social/<platform>/privacyBearerUpdate privacy setting

Email & Password

MethodEndpointAuthDescription
POST/user/verify-emailBearerSend verification email
POST/user/validate-emailTokenValidate email from link
POST/user/validate_email/v2TokenValidate email v2
POST/user/password/forgotNoneForgot password
POST/user/password/resetTokenReset password

Friends System

MethodEndpointAuthDescription
GET/user/friendsBearerGet friends list
POST/user/friends/connectBearerSend friend request
GET/user/friends/pendingBearerPending requests
POST/user/friends/updateBearerAccept/reject request

VIP & Referrals

MethodEndpointAuthDescription
GET/user/referrals/<code>NoneReferral info
GET/user/referral_code/validate/<code>NoneValidate referral code
GET/user/discord_rolesBearerUser's Discord roles
GET/user/lookup_vipAPI KeyVIP status by email

SSO (Single Sign-On)

MethodEndpointAuthDescription
POST/sso/authorizeBearerGenerate authorization code
POST/sso/tokenNoneExchange code for token
POST/sso/validateNoneValidate SSO token
GET/sso/user_rolesBearerGet user roles via SSO

2FA & Security

MethodEndpointAuthDescription
POST/user/2fa/generateBearerGenerate 2FA QR code
POST/user/2fa/verifyBearerVerify 2FA code
POST/user/check_echovault_tokenBearerCheck EchoVault token
POST/user/register_echovault_tokenBearerRegister EchoVault token

Signup

MethodEndpointAuthDescription
POST/user/signup/ethermailNoneSignup via Ethermail
POST/api/v3/user/signupNoneSignup v3
POST/api/v4/user/signupNoneSignup v4 (latest)

🖼️ NFT Data Platform

Build on top of Pentagon's cross-chain NFT data. Once a user logs in via the Identity API, your app gets access to their full profile, wallet addresses, social connections, AND their complete NFT portfolio across 15+ blockchains. No indexer setup, no RPC calls, no Alchemy/Moralis subscription needed.

What You Get After User Login

A single login gives your application access to the user's entire Pentagon ecosystem data:

👤 User Identity

Username, email, PNS name, profile picture, about me, interests. The user's Pentagon account is their universal identity.

👛 Wallet Addresses

All connected wallets: EVM (MetaMask, Rabby, Phantom), MultiversX, Tron, TON, Algorand. Plus Pentagon-generated managed wallets.

🖼️ Cross-Chain NFTs

Every NFT the user owns across Ethereum, Polygon, BSC, Pentagon Chain, SKALE, Oasys, Core, Avalanche, Arbitrum, TON, Base, Tron, and more. Synced from on-chain data by Pentagon's oracle backend.

🔗 Social Connections

Discord, Twitter/X, Telegram, LinkedIn accounts. Discord role data. Friend lists and follow relationships.

The Developer Flow

// 1. User logs in via your app (using Pentagon Identity)
const loginRes = await fetch('https://api.account.pentagon.games/user/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-PG-App-Key': 'pk_live_your_key'
  },
  body: JSON.stringify({
    type: 'email',
    username: 'player@example.com',   // or PNS name like "nftprof"
    password: '***'
  })
});
const { access_token } = (await loginRes.json()).result;

// 2. Get user profile + wallet addresses
const userInfo = await fetch('https://api.account.pentagon.games/user/info', {
  headers: { 'Authorization': `Bearer ${access_token}` }
});
// Returns: username, email, mm_address, social accounts, profile picture...

// 3. Get ALL their NFTs across every supported chain
const nfts = await fetch('https://api.account.pentagon.games/user/nfts', {
  headers: { 'Authorization': `Bearer ${access_token}` }
});
// Returns: paginated list of every NFT the user owns

// 4. Filter by collection
const gunnies = await fetch('https://api.account.pentagon.games/user/nfts?collection=Gunnies%20PFP', {
  headers: { 'Authorization': `Bearer ${access_token}` }
});

NFT API Endpoints

MethodEndpointAuthDescription
GET /user/nfts Bearer All NFTs owned by the logged-in user, across all chains
GET /user/nfts?collection=<name> Bearer Filter by collection name (comma-separated for multiple)
GET /user/nfts?name=<search> Bearer Search NFTs by name
GET /user/nfts?order_by=recently_received Bearer Sort by most recently acquired
GET /user/nfts?order_by=recently_created Bearer Sort by creation date
GET /user/penxr/nfts Bearer PenXR metaverse NFT display data (frame positions, likes)
GET /user/info_by_username/public/<username> None Public profile lookup (includes EchoVault token info)

NFT Response Format

// GET /user/nfts response
{
  "success": true,
  "result": {
    "items": [
      {
        "id": 12345,
        "token_id": "4521",
        "name": "Gunnies PFP #4521",
        "type": "ERC-721",
        "image": "https://storage.googleapis.com/...",
        "desc": "A unique Gunnies PFP character"
      },
      {
        "id": 67890,
        "token_id": "1337",
        "name": "BCSH #1337",
        "type": "ERC-721",
        "image": "https://...",
        "desc": ""
      }
    ],
    "total_item": 47,
    "total_page": 5
  }
}

Supported Collections & Chains

The Pentagon oracle syncs NFT ownership data from 15+ blockchains. Below are the active collections with NFTs currently tracked:

CollectionChainChain IDNFTs Tracked
ChainGunniesOasys555597,868
AzukiEthereum110,000
Bored Ape Yacht ClubEthereum110,000
Gunnies PFPPentagon Chain33449,093
ChainbunniesEthereum16,052
BCSH (TON)TON1818182,500
BCSH (Monad)Monad1432,498
ZombunniesEthereum11,225
BCSH (Oasys/PC)Pentagon Chain33441,124
BCSH (Polygon)Polygon1371,051
BCSH (SKALE)SKALE1482601649687+
BCSH (Arbitrum)Arbitrum42161426
BCSH (Ethereum)Ethereum1294
BCSH (Core)Core1116227
Chainguardians (Oasys)Oasys5555150
POW Bull BadgeEthereum1142
PEGNAMES (PNS)Pentagon Chain334491
BCSH (BSC)BSC5637
Missing OnezCore111632
BCSH (Avalanche)Avalanche4311415
BCSH (Tron)Tron7281264286

Total: 368,522 NFTs tracked across 2.1M+ metadata records and 353,712 ownership records. 36 collections, 112 contracts, 15+ chains.

⛓️ Supported Chains

Ethereum, Polygon, BSC, Pentagon Chain (3344), Oasys, SKALE, Core, Avalanche, Arbitrum, TON, Monad, Tron, Base

🔄 Sync Frequency

On-chain ownership synced by the nft-sync-cron oracle. Ownership changes reflected within minutes of on-chain transfer.

Use Cases

🎮 Game Integration

Let users log in and instantly see their game assets, PFPs, and collectibles. No separate NFT indexer needed. Build loot systems, marketplaces, and inventories on top of Pentagon's data.

📊 Portfolio / Analytics

Build NFT portfolio trackers, collection dashboards, or analytics tools. One login gives you the user's complete cross-chain holdings.

🎨 Creator Tools

Build tools that show creators who holds their NFTs, enable token-gated content, or create holder-only experiences.

🏆 Rewards & Airdrops

Target airdrops or rewards based on what a user holds. Verify NFT ownership for eligibility checks, Discord role assignment, or loyalty programs.

Internal / Approved Application Access

For internal Pentagon services and approved partners: Backend services that need to query NFT data across all users (not just a logged-in user) should use the NFT Metadata API at api.metadata.pentagon.games which reads from pg_nft_db. Contact the team for server-type app keys with the appropriate access level. Unauthenticated bulk access to NFT data is not available through the Identity API.

🔗 Integration Patterns

Pentagon Identity supports multiple integration patterns depending on what your app needs. Some apps need full identity + wallet verification, others just need login for data access, and some work wallet-first without requiring login at all. Choose the pattern that fits your use case.

✅ Default Approach: Embed login directly in your app. Build your own login form (email/password fields, wallet connect button) and call the Pentagon Identity API directly. Your app collects credentials, sends them to POST /user/login with your X-PG-App-Key, and receives a JWT token. The user never leaves your site.

Do NOT redirect users to pentagon.games for authentication. The API is designed for direct integration. Every pattern below assumes your app handles the login UI and makes API calls server-side or from the frontend. The SSO Widget is the only exception (drop-in popup overlay, not a redirect).

Recommended: Direct In-Page Login

Build a login form directly on your page. The form lives in YOUR HTML/React/Vue app, collects credentials, calls the API, done. No external windows, no redirects, no popups to other sites.

✅ In-Page Modal Overlay (Best)

Login form opens as a DOM overlay on your page (like pentagon.games does). User fills in email+password or connects wallet. Your code calls POST /user/login. Modal closes. Done. No external sites involved.

✅ Dedicated Login Page (Good)

A login page on YOUR domain (e.g., yourapp.com/login). Your own form, your own UI. Calls the Pentagon API from your frontend or backend.

✅ Inline Form (Good)

Login fields embedded directly in your page (sidebar, header, section). Simple and effective. Calls the API directly on submit.

🚫 Redirect to pentagon.games/sign-in (WRONG for first-party)

Never do this for your own app's login. Do not redirect to pentagon.games/sign-in. Do not open login.pentagon.games in a popup window. Those pages are for pentagon.games itself and won't return tokens to your app. (Exception: the OAuth2 SSO flow for external third-party apps — see the SSO Login Widget section below.)

🚫 Browser Popup to login.pentagon.games (WRONG)

Never do this either. Opening a new browser window/popup to login.pentagon.games causes UX issues (popup doesn't close, token doesn't pass back, blocked by popup blockers). Use an in-page form instead.

Note — this section is about FIRST-PARTY login. If you are building your own app's login UI (Pentagon's own apps, or a partner embedding login directly), use the direct in-page pattern above. The one legitimate redirect to Pentagon is the standard OAuth2 SSO flow (redirect to pentagon.games/sso-login-widget), which is intended for external third-party apps that want "Sign in with Pentagon Games." That is a different, supported pattern — see the SSO Login Widget section below.

Reference Implementation

Drop-in login components supporting all 4 sign-in methods. Uses your own App Key, no redirect needed. Full source on GitHub: pentagon-login-widget

React (Modal)
Vanilla JS (Modal)
API Only
// PentagonLoginModal.tsx — Drop-in popup login for any React/Next.js app
import { useState } from 'react';
import { useAccount, useSignMessage } from 'wagmi';
import { useConnectModal } from '@rainbow-me/rainbowkit';

const PG_API = 'https://api.account.pentagon.games';
const APP_KEY = process.env.NEXT_PUBLIC_PG_APP_KEY;

export function PentagonLoginModal({ isOpen, onClose, onLogin }) {
  const [tab, setTab] = useState('email');     // 'email' | 'wallet'
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const { address, isConnected } = useAccount();
  const { signMessageAsync } = useSignMessage();
  const { openConnectModal } = useConnectModal();

  // Email + Password login (also works with PNS name or username)
  const handleEmailLogin = async (e) => {
    e.preventDefault();
    setLoading(true); setError('');
    try {
      const res = await fetch(`${PG_API}/user/login`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-PG-App-Key': APP_KEY
        },
        body: JSON.stringify({
          type: 'email',
          username: email,   // email, PNS name, or username all work
          password,
          login_from: 'your_app'
        })
      });
      const data = await res.json();
      if (data.status) {
        onLogin(data.result);  // { access_token, refresh_token }
        onClose();
      } else {
        setError(data.message || 'Login failed');
      }
    } catch (err) { setError('Network error'); }
    finally { setLoading(false); }
  };

  // Wallet signature login
  const handleWalletLogin = async () => {
    if (!isConnected) { openConnectModal?.(); return; }
    setLoading(true); setError('');
    try {
      const message = `Logging into Pentagon Games,${Math.floor(Date.now()/1000)}`;
      const signature = await signMessageAsync({ message });
      const res = await fetch(`${PG_API}/user/login`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-PG-App-Key': APP_KEY
        },
        body: JSON.stringify({
          type: 'wallet',
          signature,
          address: address.toLowerCase(),
          message,
          login_from: 'your_app'
        })
      });
      const data = await res.json();
      if (data.status) {
        onLogin(data.result);
        onClose();
      } else {
        setError(data.message || 'Wallet not linked to an account');
      }
    } catch (err) { setError('Signature rejected or failed'); }
    finally { setLoading(false); }
  };

  if (!isOpen) return null;
  return (
    <div style={{
      position:'fixed', inset:0, zIndex:9999,
      background:'rgba(0,0,0,0.6)', backdropFilter:'blur(4px)',
      display:'flex', alignItems:'center', justifyContent:'center'
    }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()} style={{
        background:'#12121a', border:'1px solid #2a2a3a', borderRadius:16,
        padding:32, width:400, maxWidth:'90vw', color:'#e0e0e8'
      }}>
        <h2 style={{margin:'0 0 20px', fontSize:20}}>⛠ Sign In</h2>

        {/* Tabs */}
        <div style={{display:'flex', gap:0, borderBottom:'1px solid #2a2a3a', marginBottom:20}}>
          <button onClick={() => setTab('email')} style={{
            padding:'8px 20px', background:'none', border:'none', cursor:'pointer',
            color: tab==='email' ? '#7c5cff' : '#8888a0',
            borderBottom: tab==='email' ? '2px solid #7c5cff' : '2px solid transparent'
          }}>Email</button>
          <button onClick={() => setTab('wallet')} style={{
            padding:'8px 20px', background:'none', border:'none', cursor:'pointer',
            color: tab==='wallet' ? '#7c5cff' : '#8888a0',
            borderBottom: tab==='wallet' ? '2px solid #7c5cff' : '2px solid transparent'
          }}>Wallet</button>
        </div>

        {error && <div style={{color:'#ff4c6a', fontSize:14, marginBottom:12}}>{error}</div>}

        {tab === 'email' ? (
          <form onSubmit={handleEmailLogin}>
            <input placeholder="Email, PNS name, or username" value={email}
              onChange={e => setEmail(e.target.value)} style={inputStyle} />
            <input type="password" placeholder="Password" value={password}
              onChange={e => setPassword(e.target.value)} style={inputStyle} />
            <button type="submit" disabled={loading} style={btnStyle}>
              {loading ? 'Signing in...' : 'Sign In'}
            </button>
          </form>
        ) : (
          <button onClick={handleWalletLogin} disabled={loading} style={btnStyle}>
            {loading ? 'Confirming...' : isConnected
              ? `Sign with ${address.slice(0,6)}...${address.slice(-4)}`
              : 'Connect Wallet'}
          </button>
        )}

        <button onClick={onClose} style={{
          marginTop:16, background:'none', border:'none',
          color:'#8888a0', cursor:'pointer', width:'100%', textAlign:'center'
        }}>Cancel</button>
      </div>
    </div>
  );
}

const inputStyle = {
  width:'100%', padding:'10px 14px', marginBottom:12, borderRadius:8,
  border:'1px solid #2a2a3a', background:'#0a0a0f', color:'#e0e0e8',
  fontSize:14, outline:'none'
};
const btnStyle = {
  width:'100%', padding:'12px', borderRadius:8, border:'none',
  background:'#7c5cff', color:'white', fontSize:15,
  fontWeight:600, cursor:'pointer'
};

// Usage in your app:
// const [showLogin, setShowLogin] = useState(false);
// <button onClick={() => setShowLogin(true)}>Sign In</button>
// <PentagonLoginModal
//   isOpen={showLogin}
//   onClose={() => setShowLogin(false)}
//   onLogin={({ access_token }) => { saveToken(access_token); loadUser(); }}
// />
// Vanilla JS popup modal — no framework needed
// Add to any HTML page

const PG_API = 'https://api.account.pentagon.games';
const APP_KEY = 'pk_live_your_key';

function showPentagonLogin(onSuccess) {
  // Create overlay
  const overlay = document.createElement('div');
  overlay.style.cssText = `
    position:fixed; inset:0; z-index:9999;
    background:rgba(0,0,0,0.6); backdrop-filter:blur(4px);
    display:flex; align-items:center; justify-content:center;
  `;

  overlay.innerHTML = `
    <div style="background:#12121a; border:1px solid #2a2a3a; border-radius:16px;
      padding:32px; width:400px; max-width:90vw; color:#e0e0e8;">
      <h2 style="margin:0 0 20px; font-size:20px;"⛠ Sign In</h2>
      <input id="pg-email" placeholder="Email, PNS name, or username"
        style="width:100%; padding:10px 14px; margin-bottom:12px; border-radius:8px;
        border:1px solid #2a2a3a; background:#0a0a0f; color:#e0e0e8;" />
      <input id="pg-pass" type="password" placeholder="Password"
        style="width:100%; padding:10px 14px; margin-bottom:12px; border-radius:8px;
        border:1px solid #2a2a3a; background:#0a0a0f; color:#e0e0e8;" />
      <div id="pg-error" style="color:#ff4c6a; font-size:14px; margin-bottom:8px;"></div>
      <button id="pg-submit" style="width:100%; padding:12px; border-radius:8px;
        border:none; background:#7c5cff; color:white; font-size:15px;
        font-weight:600; cursor:pointer;">Sign In</button>
      <button id="pg-cancel" style="margin-top:12px; width:100%; background:none;
        border:none; color:#8888a0; cursor:pointer;">Cancel</button>
    </div>
  `;

  overlay.querySelector('#pg-cancel').onclick = () => overlay.remove();
  overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };

  overlay.querySelector('#pg-submit').onclick = async () => {
    const email = overlay.querySelector('#pg-email').value;
    const pass = overlay.querySelector('#pg-pass').value;
    const errEl = overlay.querySelector('#pg-error');
    try {
      const res = await fetch(`${PG_API}/user/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-PG-App-Key': APP_KEY },
        body: JSON.stringify({ type:'email', username:email, password:pass, login_from:'your_app' })
      });
      const data = await res.json();
      if (data.status) {
        overlay.remove();
        onSuccess(data.result);  // { access_token, refresh_token }
      } else {
        errEl.textContent = data.message || 'Login failed';
      }
    } catch(e) { errEl.textContent = 'Network error'; }
  };

  document.body.appendChild(overlay);
}

// Usage:
// document.getElementById('login-btn').onclick = () => {
//   showPentagonLogin(({ access_token }) => {
//     localStorage.setItem('pg_token', access_token);
//     loadUserData();
//   });
// };
// Server-side or API-only login (no popup needed)
// Good for backends, CLI tools, or server-rendered apps

async function pgLogin(email, password) {
  const res = await fetch('https://api.account.pentagon.games/user/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-PG-App-Key': 'pk_live_your_key'
    },
    body: JSON.stringify({
      type: 'email',
      username: email,      // works with email, PNS name, or username
      password: password,
      login_from: 'your_app'
    })
  });
  const data = await res.json();
  if (data.status) {
    return data.result;  // { access_token, refresh_token }
  }
  throw new Error(data.message || 'Login failed');
}

Login Methods (All Patterns)

Before choosing a pattern, understand the available login methods. All use POST /user/login with X-PG-App-Key header:

Methodtype fieldUsername / Data FieldsDescription
Email + Password type="email" username="user@example.com", password="***" Standard email login
PNS Name + Password type="email" username="nftprof" (lowercase PNS name), password="***" On-chain Pentagon Name Service lookup. Names are lowercase only.
Current Username + Password type="email" username="nftprof1", password="***" Direct PG username
Wallet Signature type="wallet" signature, address, message="Logging into Pentagon Games,{unix_timestamp}" Web3 wallet sign-in. Wallet must be pre-bound to account.
Magic Link (Email) POST /user/login/email with email="user@example.com" Sends a login link to email, no password needed
Ethermail SSO POST /user/login/ethermail with ethermail_token="..." Ethermail integration
SSO (OAuth2 Redirect) Redirect to /sso-login-widgetPOST /sso/authorize + /sso/token OAuth2 authorization code flow with approval screen

Pattern 1: PG Login + Wallet Match (Strict Identity)

Used by: PenDeFi (pendefi.com), Pentagon Website staking, any app where the user must transact with their registered wallet.

User logs in via PG Identity, then connects their browser wallet. The app verifies the connected wallet matches the wallet registered to their PG account. This ensures the person controlling the wallet is the verified account holder.

Flow:

  1. User logs in via any PG login method → gets JWT access_token
  2. App calls GET /user/info with Bearer token → gets mm_address (registered wallet)
  3. User connects browser wallet via RainbowKit/WalletConnect
  4. Frontend compares: if connected.toLowerCase() !== mm_address.toLowerCase() → block, show mismatch warning
  5. All transactions must come from the matched wallet
// After PG login, verify wallet match
const userInfo = await fetch('/user/info', {
  headers: { Authorization: `Bearer ${token}` }
});
const { mm_address } = (await userInfo.json()).result;

// Compare with connected wallet (from wagmi useAccount)
if (connectedAddress.toLowerCase() !== mm_address.toLowerCase()) {
  showError("Please connect the wallet registered to your Pentagon account");
  return;
}
// Wallet verified — proceed with transactions

When to use: DeFi apps, staking, token swaps, any app where on-chain actions must be tied to verified identity. Transactions go directly to smart contracts from user's wallet.

Transaction handling: Direct contract calls from user's wallet. No backend fulfillment needed. Frontend uses wagmi/viem writeContract. User pays gas from their own wallet.

Pattern 2: PG Login + Any Wallet (Flexible Wallet)

Used by: NFT Mining (mining.pentagon.games)

User logs in via PG Identity to pull their NFT data and account info, but can connect ANY wallet for mining operations. This is intentional — users shouldn't have to use their sensitive NFT-holding wallet for mining.

Flow:

  1. User logs in via PG Identity → gets JWT → app fetches their NFTs via GET /user/nfts
  2. User connects any wallet for mining (does NOT need to match mm_address)
  3. Mining rewards are tracked by the connected mining wallet, not the PG wallet
  4. NFT ownership is verified from PG account data, not from the connected wallet

When to use: Apps where NFT ownership verification is needed but the operational wallet can differ. Protects users from exposing high-value wallets.

Transaction handling: Backend-driven fulfillment. User submits mining action, backend verifies NFT ownership from PG data, and either:

Pattern 3: PG Login Only (No Wallet)

Used by: Pentagon ID Card, Pentagon.games profile pages, social features, NPC points system, internal tools

Pure PG Identity login with no external wallet connection needed. The app uses the user's internal PG wallet (also called the AA wallet / NPC wallet; field centralised_wallet_address) for any on-chain interactions, using NPC points as gas.

⚠️ READ THIS FIRST — the internal wallet lives in PG Identity, nowhere else.

The internal wallet (address + encrypted private key) is stored in the identity database user_wallet table. It is fully owned and served by the identity backend (api.account.pentagon.games). You access it with the standard login JWT — the same token login already gives you.

  • Do NOT call mining (api.mine.pentagon.games, /v2/npc/wallet) to read or spend a user's internal wallet. Mining does not own it — mining was only borrowing identity's stored wallet. Reaching into mining couples two services on an accidental auth contract and 401s the moment either side's JWT secret or token shape drifts. This caused a real outage on 2026-06-14.
  • There is nothing to "derive". The wallet already exists for every real user. Any "derive from master seed + user_id" path is vestigial fallback code that never runs for funded users. Do not copy seeds between servers. Do not re-derive addresses.
  • Each stack uses its own identity reads/signs. After a user logs in, your stack calls the identity API (read) or has the identity backend decrypt-and-sign (spend) with the user's stored key. You never reach into another product's stack (mining, gunnies, etc.) for the internal wallet.

Read flow (load the wallet + balance after login):

  1. User logs in via PG Identity → gets JWT (one token, one backend, one secret)
  2. App calls GET /user/walletinfo with Authorization: Bearer <login JWT>
  3. Response returns the internal wallet_address plus live pc_balance (queried from Pentagon Chain RPC by the identity backend) and npc_points
  4. Conversion: 1 PC = 1000 NPC points (npc_points = pc_balance × 1000)
GET https://api.account.pentagon.games/user/walletinfo
Authorization: Bearer <login JWT>

// 200 OK
{
  "wallet_address": "0xc9e2D370...",   // the internal AA/NPC wallet, from user_wallet
  "pc_balance": 2.949,                  // live PC, read from Pentagon RPC (chain 3344)
  "npc_points": 2949,                   // pc_balance × 1000
  "live_balance": true
}

Spend flow (deduct NPC / move funds from the user's internal wallet):

  1. User triggers a paid action (the card example: candy, cookie, flower, email, gift)
  2. App POSTs the action to the identity backend with the login JWT
  3. The identity backend validates the action + amount against a server-side allowlist (e.g. candy=5 NPC, gift up to 1000), resolves the recipient, then decrypts the user's stored private key (using the backend's own WALLET_PASSWORD / decrypt function) and signs the transaction itself
  4. It returns the tx hash + remaining balance

For the ID Card spend, the signed tx calls the on-chain CardSpendSplitter contract 0xf6f7b1615f7d3d2a0997848078dd200270304776, which splits each payment 30% to the recipient / 20% platform / 50% held for the spender. The contract is on-chain and not mining-owned, so any stack may call it directly with the user's stored key — no mining involvement.

POST https://api.account.pentagon.games/user/wallet/card-spend
Authorization: Bearer <login JWT>
Content-Type: application/json

{
  "action": "candy" | "cookie" | "flower" | "email" | "gift",
  "owner_handle": "nftprof",
  "amount": 5
}
// → validate (allowlist) → resolve owner wallet → decrypt spender key
//   → CardSpendSplitter.pay(owner) → returns { tx_hash, remaining_balance, split }

When to use: The Pentagon ID Card, social features, points systems, profile management, any feature that uses the user's managed internal wallet rather than a user-signed external wallet.

Transaction handling: Fully backend-managed within the identity stack. The user never signs and never sees a seed. The identity backend already has everything it needs natively — web3, the Pentagon Chain config (chain 3344, RPC), the user_wallet record, and the decrypt-and-sign function. NPC points act as the gas/spend abstraction. Zero dependency on mining or any other product backend.

Building a new stack that needs the internal wallet? The recipe is always the same: (1) authenticate with PG Identity to get the login JWT; (2) GET /user/walletinfo for the address + live balance; (3) for spends, send the action to the identity backend and let it decrypt-and-sign with the stored key. Never read the internal wallet from mining; never derive it; never copy seeds. One token, one backend.

Pattern 4: Wallet-First (PG Account Check)

Used by: Gunnies.io (gunnies.io), NFT purchase pages

User connects wallet first via RainbowKit (no PG login required). The app checks if the connected wallet belongs to a verified PG account. This allows NFT purchases without requiring full PG login — just wallet.

Flow:

  1. User connects wallet via RainbowKit modal
  2. App sends wallet signature to POST /user/login with type="wallet"
  3. If wallet is bound to a PG account → auto-login, load user data
  4. If wallet is NOT bound → user can still buy NFTs (purchase-only mode) or prompted to create account
  5. After successful wallet login, app checks mm_address match
// Wallet-first login
const message = `Logging into Pentagon Games,${Math.floor(Date.now() / 1000)}`;
const signature = await signMessageAsync({ message });
const result = await signIn('wallet-login', {
  signature,
  address: address.toLowerCase(),
  message,
  redirect: false
});
// If user has PG account → logged in
// If not → show signup or continue in wallet-only mode

When to use: NFT marketplaces, purchase flows, game clients where wallet presence matters more than full identity verification. Lowers friction for buyers.

Transaction handling: Direct contract calls from user's connected wallet. For purchases that need backend fulfillment (minting, reward distribution), backend watches for the tx hash or catches wallet + nonce to verify payment and fulfill.

Pattern 5: Wallet-First + PG Optional (Zero Friction)

Used by: NFT Mining v2 (web3 mode), any app targeting existing web3 users who don't have a Pentagon account yet.

User connects any wallet with zero PG account required. Their NFTs are loaded directly from the NFT data API by wallet address. They can use the app immediately. If they later create a PG account (or already have one), the experience upgrades automatically.

How it differs from Pattern 4: Pattern 4 (Gunnies.io) connects wallet then silently checks if it's a PG account and auto-logs in. Pattern 5 doesn't require or check for a PG account at all. The wallet IS the identity. PG login is purely an optional enhancement layer.

  1. User connects wallet via RainbowKit (no signup, no PG account needed)
  2. App loads NFTs from NFT data API: GET nft-data.pentagon.games/api/v1/nft/owner/{address}
  3. User can start using the app immediately (e.g., mining with on-chain mode)
  4. App shows CTA to create PG account for enhanced features
  5. If user creates account or logs in, experience upgrades: faster indexed NFT loading, offchain modes, free gas, PNS identity
// Pattern 5: Wallet-first, PG optional
import { useAccount } from 'wagmi';

const { address, isConnected } = useAccount();

// Load NFTs directly by wallet address (no PG login needed)
const nfts = await fetch(`https://nft-data.pentagon.games/api/v1/nft/owner/${address}`);

// Check if they have a PG account (optional enhancement)
const pgCheck = await fetch(`https://api.account.pentagon.games/user/info_by_username/public/${address}`);
const hasPGAccount = pgCheck.status === true;

if (hasPGAccount) {
  // Unlock enhanced features: offchain mining, faster data, free gas
} else {
  // Show CTA to create PG account
}

When to use: Apps targeting web3-native users who may not have a Pentagon account. Eliminates signup friction entirely. The PG account becomes a value-add, not a gate.

Transaction handling: User wallet handles all transactions directly (on-chain). For PG-enhanced features (offchain mining), backend fulfillment via managed wallet.

⚠️ Required: PG Account Upsell CTA
Every app implementing Pattern 5 MUST include a visible call-to-action explaining why users should create a Pentagon Games account. This is the conversion funnel from web3 users to the PG ecosystem. Without it, wallet-only users have no reason to join.

Recommended CTA content:
🚀 Upgrade to Pentagon Games
• Faster NFT loading from indexed database (372K+ NFTs, instant)
• Offchain mining with no gas needed
• Mine from a separate wallet (keep NFT holdings safe)
• Free starting gas ($PC) on Pentagon Chain
• PNS name as your cross-ecosystem identity
[Create Account] [Learn about PNS]

Pattern Comparison

Pattern PG Login Wallet Connect Wallet Must Match Transaction Source Example Apps
1: PG + Wallet Match Required Required Yes (strict) User wallet (direct contract) PenDeFi, Staking
2: PG + Any Wallet Required Required No (any wallet) Backend fulfillment NFT Mining
3: PG Only Required No N/A Backend (managed wallet) Profile, NPC Points
4: Wallet First Optional (auto) Required N/A User wallet (direct contract) Gunnies.io, NFT Buy
5: Wallet + PG Optional Optional (enhancement) Required N/A User wallet / backend if PG NFT Mining v2

Wallet Connection Toolkit

Pentagon ecosystem uses RainbowKit v2 + wagmi v2 + viem for wallet connections.

// Web3Provider.tsx
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import { skaleNebula, coreDao, mainnet } from 'wagmi/chains';

const PentagonChain = {
  id: 3344, name: 'Pentagon Chain',
  nativeCurrency: { name: 'PC', symbol: 'PC', decimals: 18 },
  rpcUrls: { default: { http: ['https://rpc.pentagon.games'] } },
  blockExplorers: { default: { name: 'Explorer', url: 'https://explorer.pentagon.games' } }
};

export const config = getDefaultConfig({
  appName: 'Your App Name',
  projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
  chains: [mainnet, PentagonChain, skaleNebula, coreDao],
});

Dependencies:

{
  "@rainbow-me/rainbowkit": "2",
  "wagmi": "^2.15.3",
  "viem": "2.x",
  "@wagmi/core": "^2.17.1"
}

Transaction Handling

Two models depending on who fulfills:

Direct Contract (Patterns 1 & 4):

User wallet calls contract directly. Frontend uses wagmi writeContract or sendTransaction. No backend tracking needed — contract events handle fulfillment. User pays gas.

import { useWriteContract } from 'wagmi';
const { writeContract } = useWriteContract();
await writeContract({
  address: '0xContractAddress',
  abi: contractABI,
  functionName: 'mint',
  args: [tokenId],
  value: parseEther('0.01')
});

Backend Fulfillment (Patterns 2 & 3):

Backend needs to know when user has paid or taken action. Two approaches:

  1. TX Hash tracking: User submits tx, frontend sends tx hash to backend, backend watches for confirmation
  2. Wallet + Nonce tracking: Backend knows the user's wallet and expected nonce, watches for matching tx on-chain

Backend then fulfills (mints reward, updates state, distributes tokens) using a service wallet.

// Frontend: send tx hash to backend for tracking
const tx = await writeContract({ ... });
await fetch('/api/submit-mining', {
  method: 'POST',
  headers: { Authorization: `Bearer ${pgToken}` },
  body: JSON.stringify({ txHash: tx, walletAddress: connectedAddress })
});
// Backend watches chain for tx confirmation, then fulfills

SSO Login Widget (OAuth2 Redirect Flow)

✅ For external third-party apps only ("Sign in with Pentagon Games")
Use this when an external partner app wants users to authenticate via Pentagon Games and return to their site with a token. This is a standard OAuth2 authorization code flow with an approval screen, and it is the one case where redirecting to pentagon.games/sso-login-widget is correct.

First-party Pentagon apps (faucet, gunnies, vip, mining, etc.) should NOT use this. They use the direct in-page login pattern (call POST /user/login with X-PG-App-Key) — no redirect, no popup.

How It Works

  1. Your site redirects user to https://pentagon.games/sso-login-widget with OAuth2 parameters
  2. User logs in on Pentagon Games (if not already authenticated)
  3. User sees an approval screen: "Allow [Your App] to access your Pentagon Games account?"
  4. User clicks Approve
  5. Pentagon Games redirects back to your redirect_uri with an authorization code
  6. Your backend exchanges the code for an access token via POST /sso/token

Step 1: Register Your SSO Client

Contact Pentagon Games team to register your app and get a client_id and client_secret. Provide:

Step 2: Redirect User to SSO Login Widget

// When user clicks "Sign in with Pentagon Games" on your site:
const params = new URLSearchParams({
  client_id: 'YOUR_CLIENT_ID',
  redirect_uri: 'https://yourapp.com/auth/callback',
  response_type: 'code',
  state: generateRandomState()  // CSRF protection, verify this on callback
});
window.location.href = `https://pentagon.games/sso-login-widget?${params}`;

Step 3: Handle the Callback

After user approves, Pentagon Games redirects to your redirect_uri with query params:

https://yourapp.com/auth/callback?code=AUTH_CODE_HERE&state=YOUR_STATE

Your backend extracts the code and exchanges it for a token:

// Backend endpoint: /auth/callback
const { code, state } = req.query;

// Verify state matches what you sent (CSRF protection)
if (state !== req.session.oauthState) {
  return res.status(400).json({ error: 'Invalid state' });
}

// Exchange code for token
const tokenResponse = await fetch('https://api.account.pentagon.games/sso/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-PG-App-Key': 'YOUR_APP_KEY'
  },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code: code,
    client_id: 'YOUR_CLIENT_ID',
    client_secret: 'YOUR_CLIENT_SECRET',
    redirect_uri: 'https://yourapp.com/auth/callback'
  })
});

const { access_token, refresh_token, expires_in } = await tokenResponse.json();

// Store tokens securely (server-side session or encrypted cookie)
req.session.pgToken = access_token;

// Redirect user to your app's main page
res.redirect('/dashboard');

Step 4: Use the Access Token

// Call Pentagon Identity API on behalf of the user
const userInfo = await fetch('https://api.account.pentagon.games/user/info', {
  headers: {
    'Authorization': `Bearer ${access_token}`,
    'X-PG-App-Key': 'YOUR_APP_KEY'
  }
});

const { result } = await userInfo.json();
console.log(result.username, result.mm_address, result.nfts);

Refresh Token Flow

Access tokens expire after 24 hours. Use the refresh token to get a new one without requiring the user to log in again:

const refreshResponse = await fetch('https://api.account.pentagon.games/user/token/refresh', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-PG-App-Key': 'YOUR_APP_KEY'
  },
  body: JSON.stringify({
    refresh: refresh_token
  })
});

const { access } = await refreshResponse.json();
// Update stored token
req.session.pgToken = access;

Security Best Practices


Legacy SSO Widget (Popup-based)

⚠️ Legacy — Popup Widget (Not Recommended for New Apps)
The popup-based SSO Widget below is maintained for existing SSO clients. For new integrations, use the OAuth2 redirect flow above. The redirect flow is more reliable, follows OAuth2 standards, and avoids popup blocker issues.

For existing SSO client apps that already use the popup widget:

<!-- Legacy SSO Widget — for existing SSO clients only -->
<script src="https://login.pentagon.games/static/js/pgwidget-sdk-v1.js"></script>
<pentagon-widget clientid="YOUR_SSO_CLIENT_ID"></pentagon-widget>
<script>
  window.PentagonWidget.mount();
  window.addEventListener('PentagonGamesSignInOnSuccess', (e) => {
    const token = e.detail.token;
    // User is logged in, use token for API calls
  });
</script>

The legacy widget opens a popup to login.pentagon.games, handles auth, and fires a PentagonGamesSignInOnSuccess event with the JWT token. Currently 5 SSO clients registered. New apps should use the OAuth2 redirect flow above or the in-app login pattern.

App Keys

Every application that calls the Pentagon Identity API must include an App Key header to identify itself.

How It Works

X-PG-App-Key: pk_live_your_key_here

Add this header to all requests to the Identity API. Keys are issued per-application and tracked for analytics and security.

Key Types

TypePrefixUse CaseRestrictions
Web Apppk_live_Browser-based appsOrigin-locked to registered domains
Native Apppk_live_Unity games, mobile appsNo origin restriction
Serverpk_live_Backend servicesIP-locked to registered servers
Developmentpk_test_Local developmentlocalhost only

Getting a Key

Contact the Pentagon Games team to register your application and receive a key:

You'll need to provide:

Currently 24 apps registered including Pentagon Website, Gunnies, PenDeFi, BCSH TCG, EtherFantasy, and partner integrations. Turnaround is typically within 24 hours.
Note: App keys are visible in browser dev tools for web apps. That's by design. They're origin-locked, so they can't be used from unauthorized domains. For native apps and servers, treat keys as secrets.

Database Reference

The Pentagon Identity system is backed by pg_identity_db, a dedicated PostgreSQL database. Below is a reference of all tables organized by domain.

Core Identity

Primary user records, wallets, login tracking, and profile data.

TableDescription
userPrimary user accounts (email, username, password hash, created_at, referral info)
user_walletPrimary EVM wallet binding + the user's internal PG wallet (AA / NPC wallet): address and encrypted private key. One per user. Used for login, PNS resolution, and identity-managed signing. This is the wallet mining reads — mining does not own it.
user_external_walletsAdditional wallets: MultiversX, Tron, TON, Algorand, and secondary EVM wallets
user_legacy_usernameOld/renamed usernames retained for PNS NFT airdrop mapping
user_login_historyLogin event log (timestamp, IP, method, app key used)
user_dataExtended user metadata and preferences
user_profile_pictureAvatar/profile image references
user_old_usernamesUsername change history

Social & Auth

Social platform connections, OAuth tokens, Discord role sync, and social graph.

TableDescription
user_social_accountsConnected social platforms (Discord, Twitter, Telegram, LinkedIn)
user_discord_rolesSynced Discord roles for gating and VIP status
user_friendshipFriend connections (pending, accepted, blocked)
user_followsFollow relationships between users
social_platformsPlatform definitions and OAuth configuration
user_security_accounts2FA and security key registrations

App Key System

Application registration, key tracking, and usage analytics.

TableDescription
app_registrationsRegistered applications with key, type, allowed origins/IPs
app_key_logsPer-request app key usage logs for analytics and abuse detection
external_appExternal/partner app metadata and integration config

NFT Data

Cross-chain NFT ownership and metadata, synced by backend oracle.

TableDescription
nftsIndividual NFT records (token ID, chain, contract, owner)
nft_collectionCollection definitions (name, chain, type)
nft_contractSmart contract addresses per collection per chain
nft_metadataToken metadata (name, image, attributes, rarity)
nft_ownersCurrent ownership records synced from on-chain data

Campaign & Rewards

Badge campaigns, referral tracking, and reward distribution.

TableDescription
user_campaign_badgesEarned campaign badges
user_campaign_pointsAccumulated campaign points
user_campaign_sub_badgesSub-badge progress within campaigns
user_campaign_ascended_badgesAscended (evolved) badge records
user_campaign_royal_badgesRoyal tier badge records
user_referral_detailsReferral code ownership and usage tracking
referral_rewardsReward payouts for successful referrals

PenXR / Metaverse

Metaverse user data, spaces, inventory, and virtual currency.

TableDescription
penxr_user_dataPenXR user profile and settings
penxr_user_spaceUser-owned virtual spaces
penxr_user_inventoryUser's in-world item inventory
penxr_inventoriesGlobal inventory item definitions
penxr_user_nftsNFTs displayed or used within PenXR
penxr_currencyVirtual currency definitions
penxr_currency_balanceUser currency balances

Wallet Infrastructure

Chain definitions, token lists, bridge logs, and supply tracking.

TableDescription
pgw_chainsSupported blockchain definitions (chain ID, RPC, explorer)
pgw_chain_tokensToken contracts per chain
pgw_swap_chainsChains enabled for swap/DEX features
pgw_swap_chain_tokensTokens available for swap per chain
pen_bridge_logsCross-chain bridge transaction logs
pen_supplyToken supply tracking records

SSO

TableDescription
sso_clientRegistered SSO client applications (client ID, secret, redirect URIs)

Security

Security keys, KYC, anti-sybil, and access control.

TableDescription
security_user_keysHardware security key / passkey registrations
user_kyc_statusKYC verification status per user
sybil_flagsAnti-sybil detection flags and scores
captcha_dataCaptcha challenge and verification records
blocked_email_domainsBlocked disposable/spam email domains

EchoVault

On-chain identity vault contract interactions.

TableDescription
user_echovault_contractUser's EchoVault contract deployment records
user_echovault_eventsEchoVault on-chain event log

Migration Guide

Deadline: May 28, 2026 — After this date, requests without a valid X-PG-App-Key header will be rejected with HTTP 401 on all login/signup endpoints.

Step 1: Get Your Key

Contact Pentagon Games team or check if a key has already been generated for your app.

Step 2: Add the Header

Add X-PG-App-Key to every request that hits login/signup endpoints.

Affected endpoints:

Code Examples

JavaScript
Unity / C#
Python
cURL
// Add to your login API call
const response = await fetch("https://api.account.pentagon.games/user/login", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-PG-App-Key": process.env.PG_APP_KEY,  // ← Add this
  },
  body: JSON.stringify({
    type: "email",
    username: email,
    password: password,
    login_from: "your_app",
  }),
});

const data = await response.json();
if (data.status) {
  const token = data.result.access_token;
  // Store token, use as Bearer auth for subsequent requests
}
using UnityEngine.Networking;
using System.Text;

string json = JsonUtility.ToJson(new LoginRequest {
    type = "email",
    username = email,
    password = password,
    login_from = "gunnies_pc"
});

UnityWebRequest request = new UnityWebRequest(
    "https://api.account.pentagon.games/user/login", "POST");
byte[] bodyRaw = Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("X-PG-App-Key", appKey);  // ← Add this

yield return request.SendWebRequest();
import requests
import os

response = requests.post(
    "https://api.account.pentagon.games/user/login",
    headers={
        "Content-Type": "application/json",
        "X-PG-App-Key": os.environ["PG_APP_KEY"],  # ← Add this
    },
    json={
        "type": "email",
        "username": username,
        "password": password,
        "login_from": "backend_service",
    },
)

data = response.json()
if data["status"]:
    token = data["result"]["access_token"]
curl -X POST https://api.account.pentagon.games/user/login \
  -H "Content-Type: application/json" \
  -H "X-PG-App-Key: pk_test_your_key" \
  -d '{
    "type": "email",
    "username": "test@example.com",
    "password": "password123",
    "login_from": "testing"
  }'

Timeline

DateWhat Happens
Apr 28, 2026Now CORS locked. App Keys live in soft mode (warnings only)
May 12, 2026Check-in. Review logs for apps still missing keys
May 21, 2026Final warning to any app without a key
May 28, 2026Enforcement Requests without keys rejected with 401

VIP — My Data Access

⭐ Your Data, Your Key

Pentagon VIP members can generate a personal API key to access their own data and their friends' public data programmatically.

Access your data at: vip.pentagon.games/mydata

What You Get

ScopeEndpointsDescription
self:read/user/info, /user/nfts, /user/walletinfoYour profile, NFTs, and wallet data
friends:read/user/friends, /user/friends/<username>/infoYour friends list and their public profiles
nfts:read/user/friends/<username>/nftsYour friends' NFT collections

Rate Limits

Key TypeRate Limit
VIP User Key (uk_live_)60 requests / minute
App Key (pk_live_)100 requests / minute
No key (deprecated)Will be rejected after May 28

Usage

# Use your personal API key
curl -H "X-PG-User-Key: uk_live_your_key" \
  https://api.account.pentagon.games/user/info

# Get your NFTs
curl -H "X-PG-User-Key: uk_live_your_key" \
  https://api.account.pentagon.games/user/nfts

# Get a friend's public info
curl -H "X-PG-User-Key: uk_live_your_key" \
  https://api.account.pentagon.games/user/friends/friendname/info

🏗️ Architecture & Shared API

Important: The api.account.pentagon.games domain serves both identity and payment endpoints from a single API server. This is by design, not accidental.

Why "Account" = Identity + Payments

The domain is api.account.pentagon.games, not api.identity.pentagon.games. That's because an account encompasses both who you are (identity) and what you've paid for (payments). In the accounting sense: your account is your full ledger.

What Lives on This API

CategoryEndpointsNotes
Identity /user/login, /user/signup, /user/info, /user/edit_details, /user/bind_metamask, /user/nfts, /user/auth/*, /sso/*, /user/2fa/*, etc. Auth, profiles, wallets, NFTs, social, friends, VIP, referrals
Payments /stripe/webhook, /stripe/payment_status Stripe webhook receiver + payment status checks

Server Topology

ServerRoleExternal-Facing?
pg-identity API server: identity endpoints + payment API endpoints (Stripe webhooks, payment_status). Runs pentagon-login-backend via gunicorn behind nginx. Yes — serves api.account.pentagon.games
pg-payment (workers) Internal workers only: blockchain scanner + NFT fulfillment. Holds the minter private key. No HTTP ports, no nginx. No — internal only, zero attack surface
Key design decision (May 2026): Payment API endpoints stay on pg-identity because they share the same nginx/SSL setup on api.account.pentagon.games. The payment workers (scanner, fulfillment, minter key) run on a separate isolated server with no public HTTP. They communicate only through the shared pg_payments_db database.

Future split path: If payment endpoints grow large enough to warrant their own domain (api.payment.pentagon.games), they'd move to a dedicated pg-payment-api server. The identity endpoints stay on pg-identity either way. The workers server remains isolated regardless.

Database Separation

Identity and payments use separate databases even though they share an API server:

This means the payment system can be extracted to its own server cleanly when the time comes, no schema entanglement.

Related Documentation

For payment-specific details (Stripe integration, fulfillment flow, scanner architecture), see the Payment Processor Docs.