Authentication, user profiles, wallet binding, NFT data, and social integrations for Pentagon Games ecosystem.
POST /user/login, your app gets the JWT. The user never leaves your page.POST /user/login API:X-PG-App-Key header. Currently in soft mode (warnings only). See migration guide →
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.
Contact the Pentagon Games team to get your key:
Include your App Key in all login and signup requests:
X-PG-App-Key: pk_live_your_key
Use the endpoint reference below to integrate authentication, wallets, NFTs, and social connections into your app.
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.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.
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.
Email/password, wallet signature, magic link, Ethermail SSO, social OAuth (Discord, Twitter, Telegram, LinkedIn)
EVM (MetaMask, Rabby, Phantom), MultiversX, Tron, TON, Algorand wallet binding
Cross-chain NFT ownership, metadata, collection info. Synced by backend oracle from on-chain data.
Referral codes, VIP status, Discord role sync, friends system
https://api.account.pentagon.games
All endpoints are prefixed with /user/ unless otherwise noted.
Most endpoints require a JWT Bearer token obtained from login:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
https://pentagon.games/login — Main user login pagehttps://pentagon.games/sign-in — Redirects to /login (backward compatibility)https://pentagon.games/sso-login-widget — OAuth2 SSO integration endpoint (for external apps)/sso-login-widget for OAuth2 integration.
| Method | Endpoint | Description |
|---|---|---|
| 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 |
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"
}
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).
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."]}}.
{
"status": true,
"result": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}
}
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.
king, not King). The backend normalizes input to lowercase before contract lookup.
| Method | Example | Status |
|---|---|---|
| Email + Password | nftprof@pentagon.games | Active |
| PNS Name + Password | nftprof (on-chain bound) | Active |
| Current Username + Password | nftprof1 | Active |
| Wallet Signature | MetaMask / Rabby / Phantom | Active |
| Magic Link (email) | Sends login link to email | Active |
| Ethermail SSO | Ethermail token | Active |
nftprof (as legacy of nftprof1) | Removed |
| Error | Meaning |
|---|---|
Unauthenticated user | Wrong credentials or wallet not registered |
User not verified | Email not validated yet |
Invalid Signature | Wallet signature verification failed or timestamp expired |
"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.
user.email (case-insensitive)mm_address matchesuser.username (case-insensitive)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.
There are two contexts where resolution happens, and they share the same order but differ in scope:
| Context | Used by | Scope |
|---|---|---|
| Login resolution | POST /user/login | Email + PNS + current username. Legacy username login was removed (Apr 29, 2026) — it is NOT accepted as a login credential. |
| Internal resolution | Service-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. |
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.
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
...
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.
king, not King).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.
| Field | User-facing name | Custody | Rotatable? | 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.GET /user/walletinfo returns the same value under both wallet_address and aa_wallet_address.
GET /user/info. Not an address.aa_wallet_address (PG Balance), via the identity endpoints. Never derive it from a seed, never fetch it from mining.mm_address. Read-only.penai_address.| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /user/penai/bind | Bearer | Bind or rotate the caller's PGAI address |
| GET | /user/penai/history | Bearer | Every 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..." }
}
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.
{"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.
| Cap | Value | Counts |
|---|---|---|
| Lifetime | 5 distinct addresses | Every distinct address the account has ever held |
| Rolling window | 3 binds per 30 days | History 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."}
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.
{
"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
}
}
| Field | Meaning |
|---|---|
current | The address currently bound. "" if the user has never bound one. |
addresses | Newest first. released_at: null marks the row for current; every other row carries the moment it was replaced. |
login_from | Which app performed that bind — "" when the client sent none. |
rotations_used | Count of distinct addresses ever held (matches the lifetime cap). |
rotations_remaining | 5 - rotations_used, floored at 0. |
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 valuesSent on POST /user/login, POST /user/bind_metamask and POST /user/penai/bind. On the PGAI bind it is persisted into the address history.
| Value | Client |
|---|---|
pentagon-ai-telegram | Pentagon AI on Telegram |
pentagon-ai-extension | Pentagon AI browser extension |
pentagon-ai-mobile | Pentagon AI mobile app |
pentagon-ai | Generic 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.
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.
sync/fetch hands it over exactly once, wipes it, and marks the channel delivered.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /user/sync/open | Bearer | Target opens a channel with its ephemeral public key |
| GET | /user/sync/pending | Bearer | Source lists its own pending requests (never returns ciphertext) |
| POST | /user/sync/approve | Bearer | Source posts the sealed blob |
| POST | /user/sync/reject | Bearer | Source declines (terminal) |
| POST | /user/sync/fetch | Bearer | Target 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: pending → approved → delivered, 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.
"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"}).
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /user/info | Bearer | Current user's full profile. Now includes penai_address (the PGAI wallet — empty until bound) alongside mm_address. See PGAI Wallet. |
| GET | /user/info/detail | Bearer | Limited user info |
| GET | /user/info/private | Bearer | Private user info |
| GET | /user/info_by_username/public/<username> | None | Public profile lookup. ⚠️ Resolves username + legacy only — NOT PNS. See User Resolution; do not use as a full resolver. |
| GET | /user/check_username | API Key | Check username availability |
| POST | /user/edit_details | Bearer | Update profile |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /user/bind_metamask | Bearer | Connect EVM wallet → mm_address. One-time and irreversible. |
| POST | /user/penai/bind | Bearer | Bind or rotate the PGAI wallet → penai_address. See PGAI Wallet. |
| GET | /user/penai/history | Bearer | Every PGAI address this account has held + remaining rotations |
| POST | /user/bind_multiversx | Bearer | Connect MultiversX wallet |
| POST | /user/bind_tron | Bearer | Connect Tron wallet |
| POST | /user/bind_ton | Bearer | Connect TON wallet |
| POST | /user/updateAlgorand | Bearer | Connect Algorand wallet |
| GET | /user/walletinfo | Bearer | Custodial 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-spend | Bearer | Spend NPC from the user's internal wallet (backend decrypts stored key + signs CardSpendSplitter) |
api.mine.pentagon.games) and never derive it from a seed. See Pattern 3: PG Login Only for the full read + spend flow.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /user/sync/open | Bearer | Target device opens a channel with its ephemeral public key |
| GET | /user/sync/pending | Bearer | Source device lists its own pending requests (never returns ciphertext) |
| POST | /user/sync/approve | Bearer | Source device posts the sealed blob |
| POST | /user/sync/reject | Bearer | Source device declines (terminal) |
| POST | /user/sync/fetch | Bearer | Target device polls, then reads the blob once |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /user/nfts | Bearer | User's NFTs across all chains |
| GET | /user/nfts?collection=Gunnies | Bearer | Filter by collection name |
| GET | /user/nfts?order_by=recently_received | Bearer | Sort by recent |
| GET | /user/penxr/nfts | Bearer | PenXR NFT display data |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /user/auth/discord | Bearer | Connect Discord |
| POST | /user/auth/twitter | Bearer | Connect Twitter/X |
| POST | /user/auth/telegram | Bearer | Connect Telegram |
| POST | /user/auth/linkedin | Bearer | Connect LinkedIn |
| POST | /user/social/oauth/<platform>/connect | Bearer | Generic OAuth connect |
| POST | /user/social/<platform>/disconnect | Bearer | Disconnect platform |
| PUT | /user/social/<platform>/privacy | Bearer | Update privacy setting |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /user/verify-email | Bearer | Send verification email |
| POST | /user/validate-email | Token | Validate email from link |
| POST | /user/validate_email/v2 | Token | Validate email v2 |
| POST | /user/password/forgot | None | Forgot password |
| POST | /user/password/reset | Token | Reset password |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /user/friends | Bearer | Get friends list |
| POST | /user/friends/connect | Bearer | Send friend request |
| GET | /user/friends/pending | Bearer | Pending requests |
| POST | /user/friends/update | Bearer | Accept/reject request |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /user/referrals/<code> | None | Referral info |
| GET | /user/referral_code/validate/<code> | None | Validate referral code |
| GET | /user/discord_roles | Bearer | User's Discord roles |
| GET | /user/lookup_vip | API Key | VIP status by email |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /sso/authorize | Bearer | Generate authorization code |
| POST | /sso/token | None | Exchange code for token |
| POST | /sso/validate | None | Validate SSO token |
| GET | /sso/user_roles | Bearer | Get user roles via SSO |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /user/2fa/generate | Bearer | Generate 2FA QR code |
| POST | /user/2fa/verify | Bearer | Verify 2FA code |
| POST | /user/check_echovault_token | Bearer | Check EchoVault token |
| POST | /user/register_echovault_token | Bearer | Register EchoVault token |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /user/signup/ethermail | None | Signup via Ethermail |
| POST | /api/v3/user/signup | None | Signup v3 |
| POST | /api/v4/user/signup | None | Signup v4 (latest) |
A single login gives your application access to the user's entire Pentagon ecosystem data:
Username, email, PNS name, profile picture, about me, interests. The user's Pentagon account is their universal identity.
All connected wallets: EVM (MetaMask, Rabby, Phantom), MultiversX, Tron, TON, Algorand. Plus Pentagon-generated managed wallets.
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.
Discord, Twitter/X, Telegram, LinkedIn accounts. Discord role data. Friend lists and follow relationships.
// 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}` }
});
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| 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) |
// 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
}
}
The Pentagon oracle syncs NFT ownership data from 15+ blockchains. Below are the active collections with NFTs currently tracked:
| Collection | Chain | Chain ID | NFTs Tracked |
|---|---|---|---|
| ChainGunnies | Oasys | 5555 | 97,868 |
| Azuki | Ethereum | 1 | 10,000 |
| Bored Ape Yacht Club | Ethereum | 1 | 10,000 |
| Gunnies PFP | Pentagon Chain | 3344 | 9,093 |
| Chainbunnies | Ethereum | 1 | 6,052 |
| BCSH (TON) | TON | 181818 | 2,500 |
| BCSH (Monad) | Monad | 143 | 2,498 |
| Zombunnies | Ethereum | 1 | 1,225 |
| BCSH (Oasys/PC) | Pentagon Chain | 3344 | 1,124 |
| BCSH (Polygon) | Polygon | 137 | 1,051 |
| BCSH (SKALE) | SKALE | 1482601649 | 687+ |
| BCSH (Arbitrum) | Arbitrum | 42161 | 426 |
| BCSH (Ethereum) | Ethereum | 1 | 294 |
| BCSH (Core) | Core | 1116 | 227 |
| Chainguardians (Oasys) | Oasys | 5555 | 150 |
| POW Bull Badge | Ethereum | 1 | 142 |
| PEGNAMES (PNS) | Pentagon Chain | 3344 | 91 |
| BCSH (BSC) | BSC | 56 | 37 |
| Missing Onez | Core | 1116 | 32 |
| BCSH (Avalanche) | Avalanche | 43114 | 15 |
| BCSH (Tron) | Tron | 728126428 | 6 |
Total: 368,522 NFTs tracked across 2.1M+ metadata records and 353,712 ownership records. 36 collections, 112 contracts, 15+ chains.
Ethereum, Polygon, BSC, Pentagon Chain (3344), Oasys, SKALE, Core, Avalanche, Arbitrum, TON, Monad, Tron, Base
On-chain ownership synced by the nft-sync-cron oracle. Ownership changes reflected within minutes of on-chain transfer.
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.
Build NFT portfolio trackers, collection dashboards, or analytics tools. One login gives you the user's complete cross-chain holdings.
Build tools that show creators who holds their NFTs, enable token-gated content, or create holder-only experiences.
Target airdrops or rewards based on what a user holds. Verify NFT ownership for eligibility checks, Discord role assignment, or loyalty programs.
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.
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.
POST /user/login with your X-PG-App-Key, and receives a JWT token. The user never leaves your site.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.
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.
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.
Login fields embedded directly in your page (sidebar, header, section). Simple and effective. Calls the API directly on submit.
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.)
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.
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.
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
// 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');
}
Before choosing a pattern, understand the available login methods. All use POST /user/login with X-PG-App-Key header:
| Method | type field | Username / Data Fields | Description |
|---|---|---|---|
| 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-widget → POST /sso/authorize + /sso/token |
OAuth2 authorization code flow with approval screen | |
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:
access_tokenGET /user/info with Bearer token → gets mm_address (registered wallet)connected.toLowerCase() !== mm_address.toLowerCase() → block, show mismatch warning// 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.
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:
GET /user/nftsmm_address)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:
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.
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.
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.Read flow (load the wallet + balance after login):
GET /user/walletinfo with Authorization: Bearer <login JWT>wallet_address plus live pc_balance (queried from Pentagon Chain RPC by the identity backend) and npc_pointsnpc_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):
WALLET_PASSWORD / decrypt function) and signs the transaction itselfFor 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.
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.
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:
POST /user/login with type="wallet"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.
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.
GET nft-data.pentagon.games/api/v1/nft/owner/{address}// 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.
| 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 |
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"
}
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:
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
pentagon.games/sso-login-widget is correct.POST /user/login with X-PG-App-Key) — no redirect, no popup.
https://pentagon.games/sso-login-widget with OAuth2 parametersredirect_uri with an authorization codePOST /sso/tokenContact Pentagon Games team to register your app and get a client_id and client_secret. Provide:
// 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}`;
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');
// 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);
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;
state parameter to prevent CSRF attacksclient_secret private — only use it on your backend, never expose to frontendFor 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.
Every application that calls the Pentagon Identity API must include an App Key header to identify itself.
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.
| Type | Prefix | Use Case | Restrictions |
|---|---|---|---|
| Web App | pk_live_ | Browser-based apps | Origin-locked to registered domains |
| Native App | pk_live_ | Unity games, mobile apps | No origin restriction |
| Server | pk_live_ | Backend services | IP-locked to registered servers |
| Development | pk_test_ | Local development | localhost only |
Contact the Pentagon Games team to register your application and receive a key:
You'll need to provide:
The Pentagon Identity system is backed by pg_identity_db, a dedicated PostgreSQL database. Below is a reference of all tables organized by domain.
Primary user records, wallets, login tracking, and profile data.
| Table | Description |
|---|---|
user | Primary user accounts (email, username, password hash, created_at, referral info) |
user_wallet | Primary 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_wallets | Additional wallets: MultiversX, Tron, TON, Algorand, and secondary EVM wallets |
user_legacy_username | Old/renamed usernames retained for PNS NFT airdrop mapping |
user_login_history | Login event log (timestamp, IP, method, app key used) |
user_data | Extended user metadata and preferences |
user_profile_picture | Avatar/profile image references |
user_old_usernames | Username change history |
Social platform connections, OAuth tokens, Discord role sync, and social graph.
| Table | Description |
|---|---|
user_social_accounts | Connected social platforms (Discord, Twitter, Telegram, LinkedIn) |
user_discord_roles | Synced Discord roles for gating and VIP status |
user_friendship | Friend connections (pending, accepted, blocked) |
user_follows | Follow relationships between users |
social_platforms | Platform definitions and OAuth configuration |
user_security_accounts | 2FA and security key registrations |
Application registration, key tracking, and usage analytics.
| Table | Description |
|---|---|
app_registrations | Registered applications with key, type, allowed origins/IPs |
app_key_logs | Per-request app key usage logs for analytics and abuse detection |
external_app | External/partner app metadata and integration config |
Cross-chain NFT ownership and metadata, synced by backend oracle.
| Table | Description |
|---|---|
nfts | Individual NFT records (token ID, chain, contract, owner) |
nft_collection | Collection definitions (name, chain, type) |
nft_contract | Smart contract addresses per collection per chain |
nft_metadata | Token metadata (name, image, attributes, rarity) |
nft_owners | Current ownership records synced from on-chain data |
Badge campaigns, referral tracking, and reward distribution.
| Table | Description |
|---|---|
user_campaign_badges | Earned campaign badges |
user_campaign_points | Accumulated campaign points |
user_campaign_sub_badges | Sub-badge progress within campaigns |
user_campaign_ascended_badges | Ascended (evolved) badge records |
user_campaign_royal_badges | Royal tier badge records |
user_referral_details | Referral code ownership and usage tracking |
referral_rewards | Reward payouts for successful referrals |
Metaverse user data, spaces, inventory, and virtual currency.
| Table | Description |
|---|---|
penxr_user_data | PenXR user profile and settings |
penxr_user_space | User-owned virtual spaces |
penxr_user_inventory | User's in-world item inventory |
penxr_inventories | Global inventory item definitions |
penxr_user_nfts | NFTs displayed or used within PenXR |
penxr_currency | Virtual currency definitions |
penxr_currency_balance | User currency balances |
Chain definitions, token lists, bridge logs, and supply tracking.
| Table | Description |
|---|---|
pgw_chains | Supported blockchain definitions (chain ID, RPC, explorer) |
pgw_chain_tokens | Token contracts per chain |
pgw_swap_chains | Chains enabled for swap/DEX features |
pgw_swap_chain_tokens | Tokens available for swap per chain |
pen_bridge_logs | Cross-chain bridge transaction logs |
pen_supply | Token supply tracking records |
| Table | Description |
|---|---|
sso_client | Registered SSO client applications (client ID, secret, redirect URIs) |
Security keys, KYC, anti-sybil, and access control.
| Table | Description |
|---|---|
security_user_keys | Hardware security key / passkey registrations |
user_kyc_status | KYC verification status per user |
sybil_flags | Anti-sybil detection flags and scores |
captcha_data | Captcha challenge and verification records |
blocked_email_domains | Blocked disposable/spam email domains |
On-chain identity vault contract interactions.
| Table | Description |
|---|---|
user_echovault_contract | User's EchoVault contract deployment records |
user_echovault_events | EchoVault on-chain event log |
X-PG-App-Key header will be rejected with HTTP 401 on all login/signup endpoints.
Contact Pentagon Games team or check if a key has already been generated for your app.
Add X-PG-App-Key to every request that hits login/signup endpoints.
Affected endpoints:
POST /user/loginPOST /user/login/emailPOST /user/login/ethermailPOST /user/signup/ethermailPOST /api/v3/user/signupPOST /api/v4/user/signupPOST /sso/authorizePOST /sso/token// 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"
}'
| Date | What Happens |
|---|---|
| Apr 28, 2026 | Now CORS locked. App Keys live in soft mode (warnings only) |
| May 12, 2026 | Check-in. Review logs for apps still missing keys |
| May 21, 2026 | Final warning to any app without a key |
| May 28, 2026 | Enforcement Requests without keys rejected with 401 |
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
| Scope | Endpoints | Description |
|---|---|---|
self:read | /user/info, /user/nfts, /user/walletinfo | Your profile, NFTs, and wallet data |
friends:read | /user/friends, /user/friends/<username>/info | Your friends list and their public profiles |
nfts:read | /user/friends/<username>/nfts | Your friends' NFT collections |
| Key Type | Rate 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 |
# 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
api.account.pentagon.games domain serves both identity and payment endpoints from a single API server. This is by design, not accidental.
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.
| Category | Endpoints | Notes |
|---|---|---|
| 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 | Role | External-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 |
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.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.
Identity and payments use separate databases even though they share an API server:
pg_identity_db — user accounts, auth, profiles, wallets, social, VIPpg_payments_db — payment intents, Stripe sessions, fulfillment status, mint recordsThis means the payment system can be extracted to its own server cleanly when the time comes, no schema entanglement.
For payment-specific details (Stripe integration, fulfillment flow, scanner architecture), see the Payment Processor Docs.