Developer Documentation

Everything you need to integrate with Pentagon Name Service

Contents 1. Overview 2. Contract Details 3. Name Lifecycle 4. Roles & Permissions 5. Read Functions (Resolving Names) 6. Write Functions 7. Events 8. Tier System 9. Name Checker API 10. PNS Login Integration 11. Code Examples

1. Overview

Pentagon Name Service (PNS) is an on-chain username registry on Pentagon Chain. Each name is an ERC-721 NFT with additional features: spatial address binding, URL forwarding, tier-based access control, and SBT-like locking when bound.

Names follow the format @username and resolve to EVM addresses on-chain. Off-chain, names map to profile pages at peg.gg/username and username.peg.gg.

2. Contract Details

NamePentagon Name Service
SymbolPEGNAMES
Proxy0xf97EB9f8293D1FD5587a809Eb74518c300738d07
Implementation (V5)0x5a084503746745e58e6baA6Cd5778438459919F1
ChainPentagon Chain (ID: 3344)
RPChttps://rpc.pentagon.games
Explorerhttps://explorer.pentagon.games
StandardERC-721 + AccessControl + UUPS Upgradeable
Token IDsStart at 1 (0 is never valid)
Gas TokenPC (Pentagon Chain native)

3. Name Lifecycle

Every name goes through these states:

MINT (by Moderator)
  │
  ├─ spatialAddr = 0x0 → UNBOUND (tradable NFT)
  │    │
  │    ├─ User calls rebindSpatial() → BOUND (locked, SBT-like)
  │    │    │
  │    │    ├─ Can set forwardUrl()
  │    │    ├─ Can use as PG login
  │    │    ├─ Name resolves on-chain
  │    │    ├─ CANNOT transfer or sell
  │    │    │
  │    │    └─ Moderator calls moderatorUnbind() → UNBOUND (tradable again)
  │    │
  │    └─ User transfers/sells → New owner (UNBOUND)
  │
  └─ spatialAddr != 0x0 → BOUND at mint (locked immediately)
       └─ (same as bound state above)
Key rule: Bound names are locked. Users cannot transfer a bound name. Only a Moderator can unbind a name (via support ticket). This protects users from accidentally selling an active identity.

4. Roles & Permissions

RoleIdentifierPermissions
Owner (DEFAULT_ADMIN_ROLE)0x00Rename any name. Rebind Super-High SBT addresses. Upgrade contract. Manage roles.
Moderatorkeccak256("MODERATOR_ROLE")Mint new names. Batch mint. Claim mint. Unbind names. Bind to any address (airdrops).
NFT Holder(token owner)Bind to own address only. Set forward URL (only when bound). Transfer (only when unbound).
Moderator cannot: Rename names, rebind Super-High SBTs, or upgrade the contract. These are Owner-only.

5. Read Functions (Resolving Names)

All read functions are free (no gas). Use these to resolve names in your dApp.

Name → Token ID

function tokenOfName(string name) → uint256
// Returns the tokenId for a name. Reverts if name doesn't exist.

function nameExists(string name) → bool
// Check if a name is registered. Use before tokenOfName to avoid reverts.

Token ID → Name

function nameOf(uint256 tokenId) → string
function tokenName(uint256 tokenId) → string  // same, public mapping

Name Resolution (Spatial Binding)

function spatialBinding(uint256 tokenId) → address
function spatialOf(uint256 tokenId) → address  // alias
// Returns the wallet address the name resolves to.
// Returns address(0) if unbound.

Forward URL

function forwardUrl(uint256 tokenId) → string
// Returns the forward URL for the name.
// Empty string if not set. Can only be set when bound.

Tier & Lock Status

function tokenTier(uint256 tokenId) → Tier  // 0=SuperHigh, 1=High, 2=Medium
function tierOf(uint256 tokenId) → Tier      // alias
function boundLocked(uint256 tokenId) → bool  // true if bound (locked)
function sbtUnlocked(uint256 tokenId) → bool  // true if Super-High is unlocked for transfer

Ownership

function ownerOf(uint256 tokenId) → address   // standard ERC-721
function balanceOf(address owner) → uint256    // standard ERC-721
function totalMinted() → uint256               // total names ever minted

Two-Step Name Resolution (most common pattern)

// Resolve @king to a wallet address:
uint256 tokenId = pns.tokenOfName("king");
address wallet = pns.spatialBinding(tokenId);

// Resolve @king to a forward URL:
uint256 tokenId = pns.tokenOfName("king");
string memory url = pns.forwardUrl(tokenId);

6. Write Functions

Minting (Moderator only)

function mint(address to, string name, Tier tier, address spatialAddr) → uint256 tokenId
// Mint a single name. spatialAddr=address(0) for unbound mint.

function mintBatch(address[] tos, string[] names, Tier[] tiers, address[] spatialAddrs)
// Batch mint. Arrays must be same length.

function claimMint(address to, string name, Tier tier, address spatialAddr) → uint256 tokenId
// Mint with one-per-address enforcement. Reverts if address already claimed.

Binding (NFT Holder)

function rebindSpatial(uint256 tokenId, address newAddr)
// Bind name to an address. Regular users: newAddr must equal msg.sender.
// Moderator: can bind to any address.
// Owner: can rebind Super-High SBTs.
// Binding sets boundLocked=true (locks the NFT).

Unbinding (Moderator only)

function moderatorUnbind(uint256 tokenId)
// Clears spatial binding, forward URL, and unlocks the NFT for transfer.
// Called when user submits support ticket to sell their name.

Forward URL (Bound NFT Holder only)

function setForwardUrl(uint256 tokenId, string url)
// Set forward URL. Requires: ownerOf(tokenId)==msg.sender AND boundLocked==true.
// Must bind first before setting forward URL.

Rename (Owner only)

function rename(uint256 tokenId, string newName)
// Change the name string on any token. For security/IP/compliance only.

SBT Control (Owner only)

function setSbtUnlocked(uint256 tokenId, bool unlocked)
// Unlock a Super-High SBT for one transfer. Re-locks after transfer.

Standard ERC-721

function transferFrom(address from, address to, uint256 tokenId)
function safeTransferFrom(address from, address to, uint256 tokenId)
// Standard transfers. Blocked if boundLocked==true or SBT locked.
// Clears binding + forward URL on successful transfer.

7. Events

event NameMinted(uint256 indexed tokenId, string name, Tier tier, address indexed to, address spatialAddr)
event NameRenamed(uint256 indexed tokenId, string oldName, string newName)
event SpatialBound(uint256 indexed tokenId, address indexed spatialAddr)
event NameUnbound(uint256 indexed tokenId)
event SbtLockChanged(uint256 indexed tokenId, bool unlocked)
event ForwardUrlSet(uint256 indexed tokenId, string url)
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)  // ERC-721

8. Tier System

TierEnum ValueToken TypeTradablePrice RangeBinding
Super-High0SBTNever (unless Owner unlocks)20-50 PCOwner only rebind
High1NFTWhen unbound5-20 PCUser self-bind
Medium2NFTWhen unbound1-5 PCUser self-bind
LowN/AOff-chainN/AFreeN/A

Tiers are set at mint and cannot be changed. The AI classification API determines the tier based on name rarity, cultural significance, and web presence.

9. Name Checker API

The AI classification service runs on the PNS backend and scores names in real time.

Base URL: https://id.peg.gg/api

Check a Name

POST /api/check
Content-Type: application/json

{ "name": "king", "user_key": "optional_rate_limit_key" }

Response:
{
  "name": "king",
  "score": 74,
  "tier": "high",
  "action": "reserve_premium",
  "price_pc": 16,
  "reasons": ["intrinsic score", "cultural/IP match", "web popularity"],
  "signals": {
    "length": 4,
    "is_dictionary_word": true,
    "search_result_bin": "10M+",
    "domain": { "com": "taken", "net": "taken", ... },
    "social": { "x": "taken", "instagram": "taken", ... },
    "cultural_ip_hits": [{ "kb": "Wikipedia", "id": "King", "label": "Monarch" }]
  },
  "cached": false,
  "checked_by": "venice-v1"
}

Suggest Alternatives

POST /api/suggest
{ "name": "king" }

Response:
{
  "original": "king",
  "suggestions": [
    { "name": "kingx", "tier": "low", "price_pc": 0, "available": true },
    { "name": "theking", "tier": "medium", "price_pc": 3, "available": true }
  ]
}

Stats

GET /api/stats

{ "total_classified": 68, "today_checks": 5, "daily_limit": 1000, "tiers": [...] }

Rate Limits

30 seconds between new name checks per user. 1,000 AI checks per day globally. Cached lookups are unlimited and instant.

10. PNS Login Integration

PNS names can be used as Pentagon login credentials. The resolution chain:

PNS Username → tokenOfName() → tokenId → spatialBinding() → wallet address → PG user by mm_address

Login Flow

  1. User enters PNS username + password on login form
  2. Backend calls nameExists(username) on the PNS contract
  3. If exists: tokenOfName(username) → tokenId
  4. spatialBinding(tokenId) → wallet address
  5. Lookup PG user: SELECT * FROM user WHERE mm_address = wallet
  6. Validate password, return JWT
Requirement: The name must be bound (have a spatial address) for login to work. Unbound names cannot resolve to a user.

11. Code Examples

JavaScript (ethers.js v6)

import { ethers } from 'ethers';

const RPC = 'https://rpc.pentagon.games';
const PNS = '0xf97EB9f8293D1FD5587a809Eb74518c300738d07';
const ABI = [
  'function nameExists(string) view returns (bool)',
  'function tokenOfName(string) view returns (uint256)',
  'function spatialBinding(uint256) view returns (address)',
  'function forwardUrl(uint256) view returns (string)',
  'function nameOf(uint256) view returns (string)',
  'function ownerOf(uint256) view returns (address)',
  'function boundLocked(uint256) view returns (bool)',
  'function tierOf(uint256) view returns (uint8)',
  'function totalMinted() view returns (uint256)',
  'function rebindSpatial(uint256,address)',
  'function setForwardUrl(uint256,string)',
];

const provider = new ethers.JsonRpcProvider(RPC);
const pns = new ethers.Contract(PNS, ABI, provider);

// Resolve a name to a wallet
async function resolve(name) {
  if (!await pns.nameExists(name)) return null;
  const tokenId = await pns.tokenOfName(name);
  return await pns.spatialBinding(tokenId);
}

// Get all info about a name
async function getNameInfo(name) {
  if (!await pns.nameExists(name)) return null;
  const tokenId = await pns.tokenOfName(name);
  return {
    tokenId: tokenId.toString(),
    owner: await pns.ownerOf(tokenId),
    spatial: await pns.spatialBinding(tokenId),
    url: await pns.forwardUrl(tokenId),
    tier: await pns.tierOf(tokenId),  // 0=SuperHigh, 1=High, 2=Medium
    bound: await pns.boundLocked(tokenId),
  };
}

// Bind your name (requires signer)
async function bindName(tokenId, signer) {
  const pnsWrite = pns.connect(signer);
  const tx = await pnsWrite.rebindSpatial(tokenId, await signer.getAddress());
  return tx.wait();
}

// Set forward URL (must be bound first)
async function setUrl(tokenId, url, signer) {
  const pnsWrite = pns.connect(signer);
  const tx = await pnsWrite.setForwardUrl(tokenId, url);
  return tx.wait();
}

Python (web3.py)

from web3 import Web3

RPC = 'https://rpc.pentagon.games'
PNS = '0xf97EB9f8293D1FD5587a809Eb74518c300738d07'
ABI = [...]  # Full ABI from contract

w3 = Web3(Web3.HTTPProvider(RPC))
pns = w3.eth.contract(address=PNS, abi=ABI)

# Resolve name to wallet
def resolve(name):
    if not pns.functions.nameExists(name).call():
        return None
    token_id = pns.functions.tokenOfName(name).call()
    return pns.functions.spatialBinding(token_id).call()

# Check if bound
def is_bound(name):
    token_id = pns.functions.tokenOfName(name).call()
    return pns.functions.boundLocked(token_id).call()

curl (direct RPC)

# Check if name exists
cast call 0xf97EB9f8293D1FD5587a809Eb74518c300738d07 \
  "nameExists(string)(bool)" "king" \
  --rpc-url https://rpc.pentagon.games

# Get token ID for a name
cast call 0xf97EB9f8293D1FD5587a809Eb74518c300738d07 \
  "tokenOfName(string)(uint256)" "king" \
  --rpc-url https://rpc.pentagon.games

# Get spatial binding
cast call 0xf97EB9f8293D1FD5587a809Eb74518c300738d07 \
  "spatialBinding(uint256)(address)" 1 \
  --rpc-url https://rpc.pentagon.games

Contract source code available on request. Full ABI in the Pentagon Explorer.