Centralized cross-chain payment stack for Pentagon Games ecosystem
The Pentagon Payment Processor is a centralized smart contract + backend system that handles payments across multiple chains and payment methods. Products in the Pentagon ecosystem (EtherFantasy, Gunnies, Setsuko, etc.) use the same contracts and backend for payment processing, revenue tracking, and reward fulfillment.
Beyond "how to take a payment," this stack exists so that every dollar (or PC) a user spends anywhere in the Pentagon ecosystem lands in one place they can see, query, and prove. Think of it as a block explorer scoped to a single user's spending across all of Pentagon Games: NFT mints, mining fees, NPC credits, Stripe purchases, campaign claims, cross-chain bridges, everything.
That single source of truth powers two distinct audiences, each addressed by its own section below:
The long-term goal: monitor all of a user's on-chain contract spending on Pentagon Chain and recognize what each spend was for. Anything we can't recognize is a signal: it's a service that hasn't been wired into the payment stack yet. When that happens, point the owning team (or their session agent) at the Spend Capture section and ask them to describe their payment design pattern so the tracker can incorporate it.
| Method | Chain | Contract Function |
|---|---|---|
| PC (native) | Pentagon 3344 | purchaseWithNative(skuId) |
| USDC | Ethereum | purchaseWithToken(skuId, tokenAddress) |
| PC (ERC-20) | Ethereum | purchaseWithToken(skuId, tokenAddress) |
| Stripe (fiat) | Off-chain | Stripe Checkout → webhook |
Going forward, the standard way to charge for anything in the Pentagon ecosystem is the
multichain Payment Processor (purchaseWithNative / purchaseWithToken).
Its defining benefit is multichain: the same SKU can be sold for native PC on Pentagon
Chain or USDC / PC-ERC-20 on Ethereum, through one contract and one settlement ledger
(cross_chain_purchase_log in pg_payments_db). New products should default to it.
Other rails exist for specific cases — direct P2P pay with earn-back (CardSpendSplitter), in-game action spend (PayHub), fiat (Stripe), and custodial Points spend — and are documented below so every spend lands in the same ledger. But a SKU sale is the processor.
| Rail | Use for | Chains | Entry point | Status |
|---|---|---|---|---|
| Payment Processor STANDARD | SKU sales — NFT mints, digital packs, NPC top-ups, anything with a price | Pentagon (PC) + Ethereum (USDC / PC-ERC-20) | purchaseWithNative(skuId) / purchaseWithToken(skuId, token) on 0x3930B34a… (PC) / 0xe6bde156… (ETH) |
LIVE |
Custodial Points (spend_purchase) |
Let a logged-in user pay a processor SKU straight from their PG Points balance (AA wallet) — no wallet signing | Pentagon (backend signs AA wallet → processor) | POST /user/npc/spend_purchase → purchaseWithNative(skuId) |
TO BUILD |
| CardSpendSplitter (P2P direct + earn-back) | Direct peer/creator payments that split to payee + platform + a held / earn-back bucket | Pentagon (PC) | POST /user/npc/card-spend → CardSpendSplitter.pay(payee) |
LIVE (SKU 18) |
| PayHub (GamePayHub) | In-game action spend (AR mining boosts, etc.), attributed per game id | Pentagon (PC) | POST /user/npc/spend_ar → GamePayHub.payPC(gameId, paymentId) |
LIVE |
| Stripe (fiat) | Credit-card purchases (Foundation Heroes, NPC credits) | Off-chain → webhook | Stripe Checkout → /stripe/webhook |
LIVE |
One SKU, priced independently per chain/token, bought through one contract; one backend indexes all of it.
Authoritative store: pg_payments_db (RDS) holds all payment info. The
pg_identity_db copy of these tables is legacy/frozen — never read settlement from it.
Identity touches payments only at the AA-wallet signing step.
spend_purchase (TO BUILD)
RULE
Resolve the canonical wallet from identity — never derive it. Every
Points-consuming service MUST resolve the user's AA wallet from Pentagon Identity
(user_wallet.address today, PGBalanceAccount post-AA2). Identity wallets are
random per-user (keystore under WALLET_PASSWORD, no HD seed), so any independent or deterministic
derivation yields a different address and spends the wrong balance. Cautionary example:
NFT Mining derived wallets from its own NPC_MASTER_SEED and diverged from users' real Points
wallets (now migrating to resolve-not-derive). Payments is aligned — resolve_aa_wallet
returns identity's user_wallet.address, so SKU-17 Points credit the canonical wallet.
This is the one piece not yet built, and the reason "pay with Points" is blocked on storefronts today.
It lets a logged-in user pay a processor SKU from their PG Points balance without signing: the backend
derives the user's AA wallet key and calls purchaseWithNative(skuId) server-side. It is a
sibling of the existing /user/npc/spend_ar — same custodial-signing infra, pointed at
the processor instead of PayHub. Build it as a Django endpoint on the accounts backend.
Reference implementation: user/airdrop_confirm_view.py in
pentagon-login-backend ran this exact sequence for the $CT airdrop
(6,786 allocations, zero double-pays, zero nonce collisions). Copy its ordering — the order IS the point.
def spend_purchase(user, sku_id):
# 1) IDEMPOTENCY FIRST — write the intent row BEFORE any chain action.
# UNIQUE(user_id, sku, intent_nonce) → retries/double-clicks collapse to IntegrityError.
try:
ledger = PayoutLedger.objects.create(user=user, sku=sku_id, status="pending")
except IntegrityError:
return existing_row_status(user, sku_id) # never a second send
# 2) NONCE SERIALIZATION — one Postgres advisory lock PER SENDER ADDRESS, held
# ACROSS get_nonce → sign → send (works across all gunicorn workers/boxes).
with pg_advisory_lock(key=hash64(sender_address)):
nonce = w3.eth.get_transaction_count(sender_address)
tx = processor.functions.purchaseWithNative(sku_id).build_transaction(
{"nonce": nonce, "value": price_wei, ...})
signed = w3.eth.account.sign_transaction(tx, aa_key)
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
# lock released only AFTER send — no other worker can grab the same nonce
# 3) settle ledger from the receipt (async poll): pending → confirmed/failed
ledger.mark(tx_hash)
| Trap | Rule |
|---|---|
| Double-pay on retry | Idempotency row must be written BEFORE the send, deduped by a DB UNIQUE constraint (not an if-exists check — that races). |
| Nonce collision under click-storms | The advisory lock must span get_nonce through send_raw_transaction. Lock only around get_nonce = two workers sign the same nonce; one tx silently replaces the other. |
| Timeout after send | Do NOT auto-retry the send (the tx may still land). Mark the row unknown and reconcile from the on-chain Payment event. |
| Capability | Status | Owner |
|---|---|---|
| SKU config + on-chain price (any SKU, any chain) | LIVE | payments admin (/admin/payments) |
| Direct-wallet purchase (PC / USDC / PC-ERC-20) | LIVE | processor + hosted checkout |
| Record → settle → vendor dashboard | LIVE | event_tracker + reward_service + payments.pg |
| Hosted checkout (any app links to it) | LIVE | payments.pentagon.games/pay?project=<key> |
Custodial Points payment (spend_purchase) | TO BUILD | accounts backend (Django) |
| Project-managed delivery report (project_orders + allocation) | TO BUILD | integration API + vendor |
The KEEPS card packs (tcg.etherfantasy.com) are live SKUs on the processor, sold for native PC:
| Pack | SKU | Price | Points | item_type |
|---|---|---|---|---|
| keeps_pack_x1 | 22 | 0.025 PC | 25 | digital_item |
| keeps_pack_x5 | 23 | 0.125 PC | 125 | digital_item |
| keeps_pack_x10 | 24 | 0.25 PC | 250 | digital_item |
Added via /admin/payments (register config row → owner signs setPrice on-chain →
activate). A buyer purchases at
payments.pentagon.games/pay?project=prj_tcg (PG login → "credited to @username" → pay PC).
The processor records + settles it (digital_item = no on-chain drop); EtherFantasy delivers
the packs off-chain and reports the pack/card allocation back via the delivery endpoint (TO BUILD).
Points payment for these packs lights up when spend_purchase ships.
The payment processor handles payments and fulfillment. It does not own or gatekeep NFT metadata, images, or ownership data. Understanding the boundary helps you integrate correctly.
| Not This | Use This Instead |
|---|---|
| NFT metadata storage or image hosting | NFT Data API — the centralized index for ownership, metadata, and images |
| The only way to mint | Projects can self-mint using their own backend. Grant our payment processor moderator/minter access to mint in parallel, not instead of your own minting |
| The gatekeeper for NFT data updates | Any authorized caller (your project backend, pfpvault admin panel, payment processor) can call the NFT Data API refresh endpoint directly |
| A metadata management system | After fulfillment (mint, upgrade, etc.), the caller notifies NFT Data API to refresh. Payment processor is one such caller, not the only one |
| A replacement for your project's backend | Your backend handles your product logic (upgrades, game state, etc.). Payment processor just detects payments and triggers fulfillment |
The pattern: Payment processor detects payment, calls your project webhook to fulfill, your project does its thing (mint, upgrade, whatever), then whoever did the work notifies NFT Data API to refresh. For projects that self-mint without going through payments, they call NFT Data API directly. Same endpoint, same app key.
One PaymentProcessor contract deployed per chain. Same Solidity source, same interface.
| Contract | 0x3930B34a524170Cc8966859Da167DB7B5413A0ba |
| Owner | 0xB2e3e82a95f5c4c47E30A5b420Ac4f99d32EF61f |
| Payment Token | PC (native) |
| Explorer | View on Explorer |
| Contract | 0xe6bde156369d209c4d420e966541ee17093705b5 |
| Owner | 0xB2e3e82a95f5c4c47E30A5b420Ac4f99d32EF61f |
| USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| PC (ERC-20) | 0x6c3ea9036406852006290770bedfcaba0e23a0e8 |
| Explorer | View on Etherscan |
// Read prices
mapping(uint256 => mapping(address => uint256)) public prices;
// skuConfigs[skuId] → (MintType, distributor)
mapping(uint256 => SkuConfig) public skuConfigs;
// Owner-only: configure SKUs
function setPrice(uint256 skuId, address token, uint256 price) external onlyOwner;
function setSkuConfig(uint256 skuId, MintType mintType, address distributor) external onlyOwner;
// User purchase functions
function purchaseWithNative(uint256 skuId) external payable;
function purchaseWithToken(uint256 skuId, address tokenAddress) external;
// Owner-only: withdraw collected funds
function withdrawNative() external onlyOwner;
function withdrawToken(address tokenAddress) external onlyOwner;
// Events
event PaymentReceived(uint256 indexed skuId, address indexed buyer, bool indexed minted);
event DirectMintSuccessful(uint256 indexed skuId, address indexed buyer, address distributor);
event DirectMintFailed(uint256 indexed skuId, address indexed buyer, address distributor);
| SKU | Product | Chain | Price (USD) | Fulfillment | Status |
|---|---|---|---|---|---|
2 | Tamago | PC (3344) | $10 | Centralized mint | ✅ Live |
3 | Vaelion | PC (3344) | $10 | Centralized mint | ✅ Live |
4 | No5 | PC (3344) | $10 | Centralized mint | ✅ Live |
5 | Muskl | SKALE Nebula | $50 | Centralized mint + gas bonus (0.5 PC) | ✅ Live |
7 | Monad Hero | Monad (143) | $25 | Centralized mint + gas bonus | ✅ Live |
9 | EtherFantasy Character | PC (3344) | $0.10–$10 | Project-managed (see below) | ⚠️ Project |
11 | ARB Hero — Lazuli | Arbitrum | $260 | Centralized mint + gas bonus | ✅ Live |
12 | Core Hero | Core (1116) | $10 | Centralized mint + gas bonus | ✅ Live |
13 | Polygon Hero — Avyaan | Polygon | $200 | Centralized mint + gas bonus | ✅ Live |
14 | Avax Hero — Launch | Avalanche | $50 | Centralized mint + gas bonus | ✅ Live |
15 | BNB Hero — Sentinel | BSC | $400 | Centralized mint + gas bonus | ✅ Live |
17 | 1000 NPC Credits | PC (3344) | $35 | Centralized (token send) | ✅ Live |
22 | KEEPS Pack x1 (TCG) | PC (3344) | ~$1 (0.025 PC) | Project-managed (digital_item, EF delivers) | ✅ Live |
23 | KEEPS Pack x5 (TCG) | PC (3344) | ~$5 (0.125 PC) | Project-managed (digital_item, EF delivers) | ✅ Live |
24 | KEEPS Pack x10 (TCG) | PC (3344) | ~$10 (0.25 PC) | Project-managed (digital_item, EF delivers) | ✅ Live |
The payment processor supports two fulfillment modes per SKU:
The payment processor backend mints the NFT directly using a shared minter wallet. Project grants MINTER_ROLE to the payment processor's moderator address. No project backend needed.
User pays → Scanner captures event → Fulfillment service mints NFT → Done
Requirements:
1. Deploy your NFT contract with a mintTo(address) or mintNextToken(address) function
2. Grant minter role to: 0x9A7c1e986e49C2Ace0e85Ccc6eeeb262D46b7521
3. Register SKU with contract address + mint ABI in cross_chain_payment_config
4. Set price on PaymentProcessor contract
Used by: Tamago, Vaelion, No5, Muskl, all Chain Heroes, NPC Credits
The payment processor captures the payment event for revenue tracking and referral commissions, but the project's own backend handles delivery. Use this when fulfillment requires game-specific logic (character assignment, inventory, etc.) that the centralized processor can't handle.
User pays → Scanner captures event → Record stored in pg_payments_db
(revenue tracked, referrals calculated)
→ Project's own scanner also picks up event
→ Project backend handles mint/delivery from its own DB
Requirements:
1. Project runs its own scanner watching the PaymentProcessor contract
2. Project handles minting/delivery independently
3. SKU is NOT added to cross_chain_payment_config (prevents centralized fulfillment)
4. Payment data still flows through pg_payments_db for referral/revenue tracking
Used by: EtherFantasy (SKU 9)
EtherFantasy runs its own Node.js/TypeScript backend with:
ef-scanner-pc — Watches Pentagon Chain PaymentProcessor for SKU 9 events (every 15s)ef-scanner-eth — Watches ETH mainnet PaymentProcessor for SKU 9 eventsprocessMint() — Assigns next available character, mints to buyer, tracks in local MySQL DBThe centralized scanner also captures SKU 9 events into pg_payments_db for revenue reporting and referral commission calculation. But it does not attempt to fulfill — SKU 9 has no entry in cross_chain_payment_config, so the centralized fulfillment service skips it.
This pattern works for any project with complex delivery logic. The centralized stack handles money, the project handles product.
Owner calls on each chain's contract:
// 1. Set price (per token, per chain)
setPrice(skuId, tokenAddress, priceInSmallestUnit)
// Use address(0) for native token
// 2. Configure fulfillment
setSkuConfig(skuId, mintType, distributorAddress)
// mintType: 0 = BACKEND (cross-chain, backend fulfills)
// 1 = DIRECT (same-chain, contract calls distributor.mintTo)
// 3. Add to cross_chain_payment_config table in backend DB
INSERT INTO cross_chain_payment_config
(sku_id, sku_name, item_type, item_name, amount, contract_chain_id, ...)
All internal API endpoints require an app key passed via the X-PG-App-Key header. The same keys issued for the NFT Data API work here. One key, multiple Pentagon services.
curl -H "X-PG-App-Key: pk_live_your_key" \
"https://api.account.pentagon.games/api/v1/payments?sku_id=9&status=completed"
| Auth Level | Limit | Scope |
|---|---|---|
| No key (public) | Blocked | Internal endpoints require a key |
| App key (web) | 60–100 req/min | Per key |
| App key (server) | 100–500 req/min | Per key |
Rate limits are configurable per app key. When exceeded, the API returns 429 Too Many Requests with a retry_after field. The Stripe webhook endpoint does not require an app key (authenticated by Stripe signature instead).
App keys can be issued with different access scopes:
| Scope | Access | Use Case |
|---|---|---|
payments:read | Read own payments only (wallet-bound) | User-facing frontends, Payment Explorer (user view) |
payments:read:all | Read all payments across all buyers | Admin dashboards, internal tools, Payment Explorer (admin view) |
payments:write | Trigger retries, update status | Admin fulfillment tools |
User-scoped keys (payments:read) can only query payments matching the wallet address bound to their key or authenticated session. Admin keys (payments:read:all) can query any buyer.
HEADS UP
Built vs. documented: the /api/v1/payments/* read endpoints below are a
design target, not yet built (calling /api/v1/payments/by-buyer/… returns 404 today).
What actually exists on api.account.pentagon.games: POST /payment/register,
POST /payment/status, /stripe/webhook, /stripe/payment_status, and
GET /admin/payments/query (the canonical read, auth via a dedicated PAYMENTS_READ_KEY,
not the god key). All read the live pg_payments_db via the PAYMENTS_DB
alias. Build new integrations against those; treat /api/v1/* as roadmap until marked LIVE.
Payment API endpoints currently live on api.account.pentagon.games, which is the
Pentagon Identity API
(login, user profiles, wallets, social auth, VIP). The "account" in the domain name covers both
meanings: user accounts (identity) and financial accounting (payments).
This is intentional for now. The payment endpoints (/stripe/webhook, /stripe/payment_status)
share the same nginx, SSL, and server infrastructure as the identity API on pg-identity (13.212.154.41).
The payment processing workers (on-chain scanner + NFT fulfillment + minter key) run on a separate
internal-only server pg-payment-workers (18.143.93.137) with no public HTTP ports.
| Component | Server | Domain | Facing |
|---|---|---|---|
| Payment API (webhook, status) | pg-identity (13.212.154.41) | api.account.pentagon.games |
3rd-party (Stripe, frontends) |
| Identity API (login, wallets, VIP) | pg-identity (13.212.154.41) | api.account.pentagon.games |
3rd-party (frontends, SSO clients) |
| On-chain scanner | pg-payment-workers (18.143.93.137) | None (internal) | Internal only |
| NFT fulfillment + minter key | pg-payment-workers (18.143.93.137) | None (internal) | Internal only |
When payment volume warrants its own domain, the API endpoints will move to
api.payment.pentagon.games on a dedicated server. The workers server with the
minter key stays isolated either way.
Base URL: https://api.account.pentagon.games (shared with Identity API)
Future Base URL: https://api.payment.pentagon.games (when split)
All /api/v1/payments/* endpoints require a valid X-PG-App-Key header. Same keys as the NFT Data API.
List payments with optional filters. Admin keys return all records. User keys return only payments matching their bound wallet.
| Param | Type | Description |
|---|---|---|
buyer | address | Filter by buyer wallet |
sku_id | int | Filter by SKU |
chain_id | int | Filter by chain |
status | string | pending | confirmed | completed | failed |
from | date | Start date (YYYY-MM-DD) |
to | date | End date (YYYY-MM-DD) |
page | int | Page number (default 1) |
limit | int | Per page (default 50, max 200) |
Monthly revenue summary grouped by chain, token, and SKU.
{
"total_revenue_usd": 12450.00,
"months": [
{
"month": "2026-03",
"payments": 6283,
"revenue": {
"pc_pentagon": { "amount": 1570.75, "count": 6275 },
"usdc_eth": { "amount": 64.00, "count": 8 }
}
}
],
"withdrawable": {
"pentagon": { "pc": 614.925 },
"ethereum": { "usdc": 183.17, "pc_erc20": 0 }
}
}
Get full detail for a single payment including buyer identity, on-chain proof, and delivery status. The buyer_identity block is resolved from the Pentagon Identity system (admin keys only; user keys see their own identity only).
{
"payment": {
"id": 8412,
"tx_hash": "0x7fa2cf58a278b2df6d74...",
"chain_id": 3344,
"block_number": 3350142,
"buyer": "0xcfb0aee8f7445140d49b...",
"sku_id": 9,
"sku_name": "EtherFantasy Character",
"payment_method": "native",
"payment_token": "PC",
"amount": "250000000000000000",
"amount_usd": 0.25,
"purchase_status": "confirmed",
"created_at": "2026-03-30T14:22:11+00:00"
},
"buyer_identity": {
"pg_user_id": 41892,
"username": "cryptoking",
"pns_name": "king",
"email": "king@example.com",
"wallets": [
"0xcfb0aee8f7445140d49bf589e30db43fd2ac05e9",
"0xab12...7890"
],
"social": {
"discord": { "id": "123456789", "username": "cryptoking#1234" },
"twitter": { "handle": "@cryptoking" },
"telegram": { "id": "987654321", "username": "cryptoking" }
},
"vip_tier": "VIP2",
"registered_at": "2025-06-12T08:30:00+00:00",
"last_login": "2026-05-20T14:15:22+00:00",
"resolution": "mm_address"
},
"delivery": {
"status": "completed",
"reward_tx_hash": "0x99d815d2af3e7b82...",
"reward_chain_id": 3344,
"token_id": 4201,
"contract": "0x8F83c6122Dd4d275B53a...",
"delivered_at": "2026-03-30T14:27:45+00:00"
},
"on_chain_proof": {
"payment_event_found": true,
"payment_block": 3350142,
"delivery_verified": true,
"current_nft_owner": "0xcfb0aee8f7445140d49b..."
}
}
The buyer_identity block triangulates the on-chain wallet address against the Pentagon Identity database to resolve the full user profile. Resolution checks three sources in order:
| Source | DB Field | Match |
|---|---|---|
| Primary EVM wallet | user.mm_address | Case-insensitive match against buyer address |
| Additional wallets | user_wallet table | Secondary bound wallets (MetaMask, Rabby, etc.) |
| External wallets | user_external_wallets table | MultiversX, Tron, TON, Algorand addresses |
The resolution field indicates which source matched: mm_address (primary), user_wallet (secondary), or external_wallet. If the wallet is not linked to any PG account, buyer_identity returns null with resolution: "unregistered".
The social block includes all connected accounts from the Pentagon Identity system. Use these to contact buyers for support:
| Platform | Fields | Contact Method |
|---|---|---|
email | Direct email (always present if registered) | |
| Discord | social.discord.id, social.discord.username | DM via bot or manual |
social.twitter.handle | DM or @mention | |
| Telegram | social.telegram.id, social.telegram.username | TG DM |
Social fields are only populated if the user connected that platform to their PG account. Admin keys see all fields. User keys see only their own profile (no other buyers' contact info).
| Status | Meaning |
|---|---|
pending | Payment confirmed, awaiting fulfillment |
processing | Mint transaction submitted, waiting for confirmation |
completed | NFT minted and delivered. reward_tx_hash + token_id present. |
failed | Fulfillment failed (contract error, gas, minter overloaded) |
stuck | Processing started but never completed (server crash, timeout) |
All payments for a specific wallet with delivery status for each, plus the resolved Pentagon user identity. User keys can only query their own bound wallet. Admin keys can query any address.
{
"buyer": "0xcfb0aee8f7445140d49bf589e30db43fd2ac05e9",
"buyer_identity": {
"pg_user_id": 41892,
"username": "cryptoking",
"pns_name": "king",
"email": "king@example.com",
"social": {
"discord": { "id": "123456789", "username": "cryptoking#1234" },
"twitter": { "handle": "@cryptoking" },
"telegram": { "id": "987654321", "username": "cryptoking" }
},
"vip_tier": "VIP2"
},
"total": 50,
"delivered": 44,
"pending": 0,
"failed": 6,
"page": 1,
"payments": [
{
"id": 8412,
"sku_id": 9,
"tx_hash": "0x7fa2cf58...",
"chain_id": 3344,
"amount_usd": 0.25,
"delivery_status": "completed",
"token_id": 4201,
"created_at": "2026-03-30T14:22:11+00:00"
}
]
}
All payments for a specific NFT collection (by contract address). Returns payment + delivery records for every purchase targeting this collection. Admin keys only.
| Param | Type | Description |
|---|---|---|
contract | address (path) | NFT contract address |
status | string (query) | Filter: pending | completed | failed | stuck |
from | date (query) | Start date (YYYY-MM-DD) |
to | date (query) | End date (YYYY-MM-DD) |
page | int (query) | Page number (default 1) |
limit | int (query) | Per page (default 50, max 200) |
Aggregate payment and delivery statistics. Admin keys only.
{
"total_payments": 12228,
"by_status": {
"completed": 6440,
"failed": 2571,
"stuck": 424,
"pending": 4
},
"by_sku": {
"9": { "total": 12204, "completed": 6440, "failed": 2571, "stuck": 424 },
"2": { "total": 1, "completed": 1 }
},
"on_chain_audit": {
"last_run": "2026-05-24T23:50:00Z",
"payments_on_chain": 12228,
"nfts_minted_on_chain": 6536,
"deficit": 5682,
"discrepancies": 12
}
}
Bulk resolve wallet addresses to Pentagon user identities. Admin keys only. Max 200 addresses per request. Used by the Payment Explorer for batch identity resolution.
// Request
POST /api/v1/payments/resolve-buyers
X-PG-App-Key: pk_live_admin_key
{
"addresses": [
"0xcfb0aee8f7445140d49bf589e30db43fd2ac05e9",
"0x5133657725225202a8817b8cab14aa77fd7758ef",
"0xea3339a9aac6e3c7efecd9e9c9993f3a0163d7be"
]
}
// Response
{
"resolved": 2,
"unregistered": 1,
"results": {
"0xcfb0aee8f7445140d49bf589e30db43fd2ac05e9": {
"pg_user_id": 41892,
"username": "cryptoking",
"pns_name": "king",
"email": "king@example.com",
"social": {
"discord": { "id": "123456789", "username": "cryptoking#1234" },
"telegram": { "username": "cryptoking" }
},
"resolution": "mm_address"
},
"0xea3339a9aac6e3c7efecd9e9c9993f3a0163d7be": {
"pg_user_id": 55210,
"username": "germax",
"pns_name": null,
"email": "germax@proton.me",
"social": {},
"resolution": "user_wallet"
},
"0x5133657725225202a8817b8cab14aa77fd7758ef": null
}
}
Addresses that don't match any PG account return null. The Explorer uses this to populate identity columns in the admin table and flag unregistered wallets for follow-up.
Subscribe to payment events. Receive callbacks when payments are confirmed.
{
"url": "https://vip.pentagon.games/api/payment-callback",
"events": ["payment.confirmed", "payment.completed"],
"sku_ids": [9, 17]
}
Centralized Stripe webhook endpoint. Receives checkout.session.completed events from multiple Stripe accounts.
Endpoint: https://api.account.pentagon.games/stripe/webhook
Supports multiple Stripe accounts (e.g., NPC Collections + BCSH Collections). The backend verifies the webhook signature against each configured secret until one matches. All payments write to pg_payments_db regardless of which Stripe account processed them.
When creating a Stripe Checkout Session, include these fields in metadata:
| Key | Type | Description |
|---|---|---|
skuId | int | SKU identifier (must match a registered SKU) |
wallet | address | Buyer's EVM wallet address (NFT will be minted here) |
// Success
{"status": true, "message": "payment_completed"}
// Missing metadata
{"status": false, "message": "missing_metadata"} // 400
// Invalid signature (doesn't match any configured account)
{"status": false, "message": "Invalid signature"} // 400
To onboard a new project's Stripe account to the centralized webhook:
whsec_...).env as STRIPE_WEBHOOK_SECRET_<PROJECT>STRIPE_WEBHOOK_SECRETS list in settings.pyhttps://api.account.pentagon.games/stripe/webhook| Account | SKUs | Status |
|---|---|---|
| NPC Collections | SKU 17 (NPC Credits) | ✅ Live |
| BCSH Collections | SKUs 2, 3, 4 (Tamago, Vaelion, No5) | ✅ Live |
Check payment status for a Stripe purchase. Reads from pg_payments_db. Works for any SKU regardless of which Stripe account processed it.
| Param | Type | Description |
|---|---|---|
sku_id | int | SKU identifier |
order_id | string | Stripe checkout session ID (cs_live_...) |
buyer | address | Buyer's wallet address |
// Found
{
"success": true,
"result": {
"id": 12257,
"tx_hash": "cs_live_a1ccPzX7...",
"chain_id": 8888,
"block_number": 0,
"buyer": "0x1fdf1637...",
"sku_id": 3,
"reward_status": "completed", // pending | processing | completed | failed
"payment_method": "stripe",
"purchase_status": "confirmed",
"created_at": "2026-05-18T08:04:47.561900+00:00",
"updated_at": "2026-05-20T03:22:11.291272+00:00",
"reward_tx_hash": "0x99d815d2...", // mint transaction hash (null if not yet fulfilled)
"reward_chain_id": 3344 // chain where NFT was minted
}
}
// Not found
{"status": false, "message": "Data Not found"}
| Status | Meaning |
|---|---|
pending | Payment confirmed, awaiting fulfillment (within 5 minutes) |
processing | Fulfillment in progress (minting transaction sent) |
completed | NFT minted and delivered. reward_tx_hash contains the mint transaction. |
failed | Fulfillment failed (contract error, gas issue, etc.) |
A web-based payment lookup tool for buyers and admins. Gated by Pentagon login (same auth as nftmining.com). No public access, no crawling.
https://payments.pentagon.games LIVE
| Role | Access | Auth |
|---|---|---|
| User (payer) | View own payments and delivery status only. Filtered by connected wallet. | Pentagon login (wallet connect or social login) |
| Admin | View all payments, all buyers, all SKUs. Export, retry failed, bulk actions. | Pentagon login + admin whitelist (initially: nftprof) |
After logging in, the user sees all payments from wallets linked to their Pentagon account:
My Payments
| Payment ID | Date | SKU | Amount | Delivery | TX |
|---|---|---|---|---|---|
#8412 |
Mar 30, 2026 | EtherFantasy Character | 0.25 PC | ✅ Delivered | View |
#8413 |
Mar 30, 2026 | EtherFantasy Character | 0.25 PC | ❌ Failed | View |
Clicking a payment opens the detail view showing:
Admins see everything the user sees, plus full buyer identity resolution and contact info:
Admin Payment Explorer
| ID | Date | Buyer | PG User | Contact | SKU | Delivery | On-Chain |
|---|---|---|---|---|---|---|---|
#8412 |
Mar 30 | 0xcfb0...05e9 |
cryptoking (ID: 41892) | 📧 💬 🐦 | SKU 9 | ✅ | ✅ |
#9001 |
Mar 30 | 0x5133...8ef |
Unregistered | — | SKU 9 | ❌ Failed | ✅ |
For every payment, the Explorer resolves the buyer wallet to a Pentagon user account by querying the Identity API. This gives admins a complete picture of who paid and how to reach them:
If the wallet is not linked to any Pentagon account, the identity block shows "Unregistered". The admin can still see the wallet address and on-chain payment proof, but has no contact info. For bulk outreach, export the unregistered wallet list and consider on-chain messaging or explorer-based contact.
The explorer cross-references DB records against on-chain truth, similar to the NFT Data API's four-layer sync engine:
robots.txt + noindex meta tags).| Component | Choice | Reason |
|---|---|---|
| Frontend | Next.js (React) | Same stack as pentagon.games, nftmining.com |
| Auth | Pentagon Identity API | Same SSO as nftmining.com, wallet-linked sessions |
| Data | Payment API (/api/v1/payments/*) | All data served through the authenticated API, not direct DB |
| Hosting | pg-fe (18.139.44.212) or dedicated | Same infra pattern as other Pentagon frontends |
mintTo(address) or mintNextToken(address) functionMINTER_ROLE to moderator address: 0x9A7c1e986e49C2Ace0e85Ccc6eeeb262D46b7521setPrice()setSkuConfig()cross_chain_payment_config table (contract address, chain, mint ABI)purchaseWithNative() or purchaseWithToken()setPrice()BACKEND mode via setSkuConfig()cross_chain_payment_config (prevents centralized fulfillment)PaymentReceived events with your SKU IDpg_payments_db for revenue and referral calculationsmetadata: { skuId, wallet }https://api.account.pentagon.games/stripe/webhookPOST /stripe/payment_status to poll fulfillment status from your frontend
// Purchase with native PC on Pentagon Chain
const { writeContract } = useWriteContract();
await writeContract({
address: '0x3930B34a524170Cc8966859Da167DB7B5413A0ba',
abi: paymentProcessorAbi,
functionName: 'purchaseWithNative',
args: [9n], // SKU ID
value: parseEther('0.25'), // Must match contract price exactly
chain: pentagonChain,
});
// Purchase with USDC on Ethereum
// Step 1: Approve USDC spend
await writeContract({
address: USDC_ADDRESS,
abi: erc20Abi,
functionName: 'approve',
args: [PAYMENT_PROCESSOR_ETH, 8000000n], // $8 USDC
});
// Step 2: Purchase
await writeContract({
address: PAYMENT_PROCESSOR_ETH,
abi: paymentProcessorAbi,
functionName: 'purchaseWithToken',
args: [9n, USDC_ADDRESS],
});
When a payment is confirmed, the payment processor notifies the VIP referral system. If the buyer was referred by a VIP member, a commission is calculated based on the referrer's tier.
| VIP Tier | Commission Rate |
|---|---|
| VIP1 | 0% (view only) |
| VIP2 | 5% |
| VIP3 | 15% |
| Partner | 20% (custom) |
| Event | Commissionable |
|---|---|
| ETH chain NFT purchases | ✅ Yes |
| Stripe/PayPal payments | ✅ Yes |
| ETH → PC Bridge deposits | ✅ Yes |
| Cross-chain purchases (USDC) | ✅ Yes |
| PC-on-PC purchases | ❌ No (internal) |
The /api/v1/payments/summary endpoint provides monthly revenue breakdowns.
Funds can be withdrawn by the contract owner via withdrawNative() and
withdrawToken().
| Pentagon Chain Contract | 614.925 PC |
| ETH Contract (USDC) | $183.17 |
| ETH Contract (ETH) | 0 |
| ETH Contract (PC ERC-20) | 0 |
Last updated: 2026-05-02
The Campaign Rewards system incentivizes user activity by compensating purchases with on-chain token rewards. Think of it as an on-ramp: users buy NPC credits (fiat via Stripe), and the system rewards them with real tokens on Pentagon Chain and Ethereum mainnet. Campaigns are time-limited, one-per-user, and publicly visible on the Offers tab at payments.pentagon.games.
NPC PG Points are PC tokens held in the user's AA (Account Abstraction) internal wallet. 1000 NPC = 1 PC. When a user purchases NPC credits via Stripe on pentagon.games/account, they're buying PC into their platform wallet.
Each campaign that distributes tokens on ETH mainnet gets a dedicated smart contract. The contract enforces the 1-per-address rule on-chain, preventing double claims even if the backend has a bug. After the campaign ends, remaining funds can only be withdrawn by the designated recovery address.
| Owner | 0xC486efE8F196Ed9956b54965988D3307e08cBde0 (Agent deployer) |
| Recovery Address | 0xB2e3e82a95f5c4c47E30A5b420Ac4f99d32EF61f (Idon/dev) |
| Chain | Ethereum Mainnet (1) |
| 1-per-address | mapping(address => bool) hasClaimed |
// Owner-only: send reward to qualified user (1 per address enforced)
function distribute(address recipient) external onlyOwner;
// Recovery: withdraw remaining funds after campaign ends
// Only callable by recoveryAddress after campaignEnd timestamp
function recoverFunds() external;
// Pause/unpause distributions
function setPaused(bool _paused) external onlyOwner;
// View functions
function hasClaimed(address user) external view returns (bool);
function remainingFunds() external view returns (uint256);
function totalClaimed() external view returns (uint256);
function campaignEnd() external view returns (uint256);
function rewardAmount() external view returns (uint256);
// Events
event Claimed(address indexed recipient, uint256 amount, uint256 timestamp);
event FundsRecovered(address indexed to, uint256 amount);
event CampaignPaused(bool paused);
Four new tables in pg_payments_db alongside the existing payment tables:
| Table | Purpose | Key Fields |
|---|---|---|
campaign |
Campaign definitions (admin/agent-created) | slug, title, trigger_type, trigger_threshold, starts_at, ends_at, max_claims (per user), is_active |
campaign_reward |
What each campaign pays out (supports multiple rewards per campaign) | campaign_id, reward_type (native_token/erc20/nft), chain_id, amount, token_symbol, is_bonus, contract_address |
campaign_claim |
Who claimed what (1-per-user enforced via unique constraint) | campaign_id, user_id, wallet_address, status, qualification_proof (JSONB), UNIQUE(campaign_id, user_id) |
campaign_claim_reward |
Individual TX tracking per reward delivery | claim_id, campaign_reward_id, chain_id, amount, to_address, tx_hash, status |
List active campaigns. No auth required. Powers the Offers tab on payments.pentagon.games.
[
{
"slug": "npc-1000-pc-bonus-june2026",
"title": "Earn 1000 NPC Points → Get PC on Ethereum",
"description": "Purchase 1000 NPC PG Points and receive...",
"image_url": "https://pentagon.games/assets/campaigns/npc-pc-bonus.png",
"starts_at": "2026-06-01T00:00:00Z",
"ends_at": "2026-06-30T23:59:59Z",
"is_featured": true,
"rewards": [
{ "label": "1 PC on Pentagon Chain", "chain_id": 3344, "amount": 1.0, "is_bonus": false },
{ "label": "1 PC on Ethereum", "chain_id": 1, "amount": 1.0, "is_bonus": true }
],
"trigger": { "type": "npc_credit", "threshold": 1000 },
"total_claims": 42
}
]
Check a user's eligibility and reward status for a specific campaign.
{
"eligible": true,
"claimed": true,
"claim": {
"status": "completed",
"qualified_at": "2026-06-05T14:30:00Z",
"rewards": [
{
"label": "1 PC on Pentagon Chain",
"chain_id": 3344,
"amount": 1.0,
"status": "success",
"tx_hash": "0xabc...",
"explorer_url": "https://explorer.pentagon.games/tx/0xabc..."
},
{
"label": "1 PC on Ethereum",
"chain_id": 1,
"amount": 1.0,
"status": "success",
"tx_hash": "0xdef...",
"explorer_url": "https://etherscan.io/tx/0xdef..."
}
]
}
}
Called when a user meets a campaign threshold. Creates a claim entry. Returns 409 if already claimed.
// Request
{
"campaign_slug": "npc-1000-pc-bonus-june2026",
"user_id": "user179298482",
"wallet_address": "0x1234...",
"proof": { "npc_points": 1000, "stripe_pi": "pi_xxx" }
}
// Success: 201 Created
{ "claim_id": 42, "status": "qualified" }
// Already claimed: 409 Conflict
{ "error": "already_claimed", "claimed_at": "2026-06-05T14:30:00Z" }
The Offers tab on payments.pentagon.games shows all active and past campaigns. Users can:
| Campaign | Earn 1000 NPC Points → Get PC on Ethereum |
| Trigger | Acquire 1000 NPC PG Points (= 1 PC in AA wallet) |
| Reward 1 | 1 PC on Pentagon Chain (3344) |
| Reward 2 (Bonus) | 1 PC on Ethereum mainnet (via CampaignReward contract) |
| Limit | 1 per user (enforced in DB + on-chain) |
| Period | June 1 – June 30, 2026 |
| Post-campaign | Unclaimed ETH funds recoverable only by 0xB2e3...EF61f |
Campaigns are created by the agent directly (no admin panel). After the first campaign, an agent guide will be written for standardized campaign creation. The process:
This section is written to be handed directly to another agent or developer who runs a
service that takes money from users but does not go through the standard payment processor or Stripe.
The goal is simple: get your spend events into pg_payments_db so the user's unified spending
history (and their AI agent) can see and prove every payment they made.
Why your payment data needs to be provided. Pentagon is building a single per-user spending ledger — effectively a block explorer of a user's spending across the whole ecosystem. If your service collects fees and we don't capture them, the user sees a gap: they paid, but their history doesn't show it, and their agent can't explain it. Every spend that isn't recognized is treated as an unconnected service. Connecting yours closes the gap.
The NFT mining product is the reference case. It does most things itself: a custom contract takes a fee from the user to initiate mining and distributes rewards (mega pool, omega pool, treasury split). It is fully self-contained — it does not use the payment processor contract and it does not use Stripe. That's fine. We don't need to take over its fee logic. We only need to capture the spend events so the user has one place to track and prove their mining payments alongside everything else.
When asked to integrate, describe your payment design pattern using the items below. This is the exact information the tracker needs to ingest and correctly label your spend events:
| # | What to provide | Why the tracker needs it |
|---|---|---|
| 1 | Contract address(es) + chain ID that receive user funds | So the scanner knows what to watch. One row per (address, chain). |
| 2 | The fee/payment event signature emitted on a successful spend (name + indexed args) | The scanner subscribes to this event. Ideally it carries the payer address and the amount kept. e.g. FeePaid(address indexed payer, uint256 amount, uint8 tier) |
| 3 | Which field is the payer/user address | This is the key the ledger groups by, and what resolves to a Pentagon identity (see buyer resolution in Section 6). |
| 4 | Amount semantics — token, decimals, and whether the event amount is the amount kept vs amount sent (refunds?) | So the recorded spend equals what the user actually paid, not an over- or under-count. (Mining had exactly this kept-vs-sent nuance.) |
| 5 | What the spend is for — a human label + a stable type/category (e.g. mining_fee, boost_7day) |
So the explorer can show "Mining boost (7-day)" instead of an unknown transfer, and so unrecognized spends can be flagged. |
| 6 | Any sub-type / tier dimension (e.g. 24h vs 7-day, mega vs omega) | Lets the ledger and revenue reporting attribute by tier. Emitting a distinct event or a tier arg makes this clean. |
| 7 | Outcome/fulfillment reference, if any (reward tx, token id, raffle entry) | So "what happened after I paid" can be answered end-to-end, not just "you paid X". |
The cheapest way to be trackable is to emit one explicit event per paid action, carrying the payer, the amount actually kept, and a tier/type. Computing on the amount kept (after any refund of excess) keeps the ledger consistent with on-chain reality.
// Recommended: distinct, attributable spend event
event FeePaid(
address indexed payer, // (3) user address -> resolves to PG identity
uint256 amount, // (4) amount KEPT, in token's smallest unit
uint8 tier // (6) e.g. 0 = 24h, 1 = 7-day
);
// Emit AFTER refunding excess, so amount == what the user actually paid:
// uint256 kept = boostFee;
// if (msg.value > kept) payable(msg.sender).transfer(msg.value - kept);
// emit FeePaid(msg.sender, kept, tier);
If retrofitting an existing contract isn't possible, you can instead provide a backend feed (a webhook or a list of tx hashes + payer + amount + type) and we'll ingest that. On-chain events are preferred because they're self-proving.
Once you provide the checklist above, the spend tracker watches your contract the same way the payment
processor's own scanners (ef-scanner-pc / ef-scanner-eth) watch the processor contract:
Unrecognized-spend detection: the tracker can also watch a user's general contract activity on Pentagon Chain. Any spend that doesn't match a registered service surfaces as "unknown." That's the trigger to find the owning team and walk them through this checklist.
This section explains how a user's own AI agent reads payment information to help that user
understand and debug their spending. When a user runs a Pentagon AI agent, the agent is granted access to the
user's mydata endpoint, which exposes everything a logged-in user could themselves access across
the Pentagon ecosystem.
Endpoint: https://vip.pentagon.games/mydata
Scope: one authenticated user's own data only — profile, NFTs, internal (AA) wallet,
and payment/spend history. The agent never sees other users' data through this path.
| Domain | Contents | Backed By |
|---|---|---|
| Profile / identity | username, PNS name, email, linked socials, VIP tier, wallets | Pentagon Identity API |
| NFT data | owned NFTs, metadata, images | NFT Data API |
| Internal wallet | AA wallet address, balances, NPC credits | Identity / wallet service |
| Payment / spend history | every captured spend: processor payments, Stripe, mining fees, campaign claims, custom-integration spends | This payment stack (pg_payments_db) |
The payment portion of mydata is the user-scoped view of the same data described in Section 6,
bound to the authenticated user's wallet(s). Conceptually it returns (or links through to) the
payments:read view: only payments matching that user's bound wallet(s) or session.
// The user's agent calls mydata on the user's behalf (session-authenticated)
GET https://vip.pentagon.games/mydata
// Payment section (user-scoped; equivalent to /api/v1/payments/by-buyer/{own wallet})
{
"payments": {
"total": 12,
"delivered": 11,
"pending": 0,
"failed": 1,
"items": [
{
"id": 8412,
"type": "mining_fee", // label from the capturing integration
"label": "Mining boost (7-day)",
"chain_id": 3344,
"amount": "100000000000000000", // amount actually kept
"amount_usd": 0.25,
"tx_hash": "0x7fa2cf58...",
"explorer_url": "https://explorer.pentagon.games/tx/0x7fa2cf58...",
"status": "completed",
"outcome": { "reward_tx_hash": "0x99d8...", "tier": "7day" },
"created_at": "2026-06-02T03:34:50Z"
}
]
}
}
A typical debugging flow the agent runs for its user:
GET /mydata and reads the payments section (user-scoped, no admin key).status and outcome.unknown (captured on-chain but unlabeled), that's the signal a service
isn't wired into the stack yet — the agent can flag it, and a builder should follow
Section 11 to register it.
Security boundary: mydata is always scoped to the authenticated user. The
agent inherits the user's own visibility — it can see that user's contact fields and full spend
history, but never another buyer's. Admin-wide queries (payments:read:all) are a separate,
non-user path covered in Section 5.