Tokens, keys, and the chain — the decisions you don't get a second attempt at
NAQD and NAQD Minting & Reserves
both document a running system that assumes a mint already exists. This page is about
what happens before that: who is allowed to create NAQD supply, which Solana
token program the mint is built on, and why Solana was picked as the primary chain at
all. All three are one-way doors — get any of them wrong and the fix is a new mint, not a
patch. docs/TOKEN_REGISTRY.md is the operational record for the first two;
this page explains the reasoning behind all three and verifies every claim in it against
the actual code that will run.
The token registry
docs/TOKEN_REGISTRY.md is not design documentation — it is the platform's living
operational record of every NAQD deployment: the mint addresses that exist, the account that
holds each authority, and where the keys controlling them physically live. It contains no
secrets; it records where things live and who holds them, never key
material itself. It is meant to be edited the same day something changes, not "eventually" —
a registry that lags reality is worse than no registry, because it actively misleads whoever
reads it next during an incident.
An on-chain mint that exists but is absent from this table is not an undocumented deployment — it is an incident. Nothing should ever be able to mint NAQD supply that isn't also traceable in this file. Update it immediately when: a mint is created, an authority is transferred, a key is rotated or regenerated, or a compromise is even suspected — before remediation is finished, not after.
Current state — reproduced from docs/TOKEN_REGISTRY.md §1
| Network | Mint address | Decimals | Token program | Mint authority | Freeze authority | Created | Status |
|---|---|---|---|---|---|---|---|
| solana-devnet | not yet created | 6 | SPL Token | BfEh8Cy…6kRz | BfEh8Cy…6kRz | — | treasury generated, awaiting faucet funding |
| solana-mainnet | not yet created | — | decide first (§token-program) | — | — | — | blocked on §token-program decision + reserve |
| polygon | not yet created | — | ERC-20 | — | — | — | secondary chain, not started |
As of this page's generation date, zero mints exist anywhere. The devnet treasury below is a signer, not a mint — it's the keypair that will become the devnet mint/freeze authority the moment naqd-token create runs.
BfEh8CyUomktmZE9nivH9YStPuxyfqryjrDSMNUM6kRz~/.mynex/keys/naqd-devnet-treasury.json (mode 0600, outside the repo)What gets written to §1, and when, follows directly from the two meanings of "minting": the registry tracks token creation events (one row per chain, ever), not the recurring issuance operations that stablecoin-service posts to the ledger millions of times over. Concretely:
- The moment
naqd-token createsucceeds. Record the mint address, decimals, token program, and both authorities in §1 — the command prints all of them on success. - The moment an authority moves.
spl-token authorize(see Operating & rotating) changes who can sign for mint or freeze — the new authority and who approved the change belong in the same row. - The moment a key is rotated or a keypair file is regenerated, even if the on-chain authority hasn't moved yet — §2's custody table should never describe a file that no longer exists.
- The moment a compromise is even suspected. Don't wait for confirmation — see the incident ordering in Operating & rotating.
Key custody model
Three keys show up across this platform's NAQD path, and they have genuinely different blast radii — worth being exact about which is which, since the most common mistake is treating them as interchangeable "the crypto key."
internal/blockchain-service/keys.go and registry.go.How the service actually loads and decrypts keys
Treasury private keys for both live chains are AES-256-GCM encrypted at rest. The easy
mistake is assuming CHAIN_ENV is somehow part of key derivation
— it isn't. It only selects which ChainClient implementation gets
constructed:
func NewRegistryFromConfig(cfg *config.Config) (*Registry, error) {
mode := strings.ToLower(strings.TrimSpace(os.Getenv("CHAIN_ENV")))
if mode == "" { mode = ChainEnvSandbox }
switch mode {
case ChainEnvLive:
solanaC, err := newSolanaChain(cfg) // hard-fails if keys/addresses unset
polygonC, err := newPolygonChain(cfg) // same — never silently falls back
return NewRegistry([]ChainClient{solanaC, polygonC}, "solana")
case ChainEnvSandbox:
return NewRegistry([]ChainClient{NewSandboxChain("solana"), NewSandboxChain("polygon")}, "solana")
}
}
CHAIN_ENV=live hard-fails construction at startup if either chain's keys or addresses are missing — it deliberately never degrades to sandbox quietly, because a live deployment must not end up minting into an in-memory map without anyone noticing.
func deriveAESKey(passphrase string) []byte {
sum := sha256.Sum256([]byte(passphrase))
return sum[:] // ENCRYPTION_KEY, whatever length an operator sets, becomes exactly 32 bytes
}
func decryptTreasuryKey(passphrase, ciphertextB64 string) (string, error) {
key := deriveAESKey(passphrase)
raw, _ := base64.StdEncoding.DecodeString(ciphertextB64)
block, _ := aes.NewCipher(key)
gcm, _ := cipher.NewGCM(block)
nonce, ciphertext := raw[:gcm.NonceSize()], raw[gcm.NonceSize():]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
return string(plaintext), err
}
Called as decryptTreasuryKey(cfg.Security.EncryptionKey, encrypted) — identically for SOLANA_WALLET_PRIVATE_KEY and POLYGON_WALLET_PRIVATE_KEY inside newSolanaChain/newPolygonChain — the same ENCRYPTION_KEY that also wraps card PANs (Security → Key management). In CHAIN_ENV=sandbox none of this runs at all; no keys are needed. Custody address = treasury address in both live implementations — there is no separate "mint authority" identity distinct from the treasury signer anywhere in this codebase today; that separation only exists once a multisig becomes the mint authority (§chain-choice does not change this — it's orthogonal to which chain is primary).
The .gitignore protections, and what a leak actually means
# --- Chain keypairs: NEVER commit these (they are mint authorities) ---
*treasury*.json
*keypair*.json
!package*.json
The negation on the last line matters: without it, the two globs above would also swallow every package.json/package-lock.json in the admin dashboard and mobile tooling, which is not the intent.
Not "a credential that grants access to" — is. Solana/Ethereum treasury keys are
bearer secrets: whoever holds the file can sign as that address, full stop, with no
additional password or MFA step. naqd-token keygen prints this outright the
moment it generates one: "This file IS the mint authority. Anyone holding it can issue
NAQD." If one is ever committed — even to a private repo, even briefly, even deleted in
a later commit — treat it as fully compromised: create a new mint and move the reserve
narrative forward from there. Do not merely rotate the authority on the same mint; git
history doesn't forget.
SPL Token vs. Token-2022
The single irreversible decision that has to happen before any mainnet mint exists. A Solana mint is created under exactly one token program and cannot be migrated to the other afterwards; Token-2022's extensions can only be enabled at mint creation, never bolted on later. Getting this wrong doesn't mean a config change — it means creating a second mint and migrating every holder's balance to it.
| Capability | SPL Token (original) | Token-2022 | Why it matters for NAQD |
|---|---|---|---|
| Freeze / thaw accounts | ✅ | ✅ | Put a compliance hold on one holder's account without touching anyone else's |
| Permanent delegate (clawback / court-ordered seizure) | ❌ | ✅ — set at creation, permanent | Moves or burns any holder's tokens without their signature — see the callout below |
| Confidential transfers | ❌ | ✅ | Balance/amount privacy — not an AML requirement here, and it adds real engineering surface |
| Transfer hooks (e.g. KYC gating on every transfer) | ❌ | ✅ | A program runs on every transfer and can block non-KYC'd counterparties |
| On-chain metadata pointer | via Metaplex (external program) | ✅ native | Affects how wallets resolve NAQD's name/icon — cosmetic, but a real dependency either way |
| Wallet / exchange support | universal | broad by 2026, still marginally behind | Worth checking the specific wallets/exchanges in the platform's actual reach before committing |
| Migrating between programs later | not possible, either direction | The decision below is made exactly once, at mint creation, forever | |
Setting a permanent delegate grants that authority the power to move or burn any
holder's tokens without their signature — the on-chain equivalent of a bank's ability to
reverse a fraudulent transfer, except unilateral and irreversible once set. For an
asset-backed instrument facing AML obligations, that's exactly the primitive regulators tend
to ask for, and counsel may require it. It is also the extension most likely to draw
scrutiny from holders who chose NAQD's on-chain withdrawal path specifically for
self-custody guarantees — a permanent delegate means "self-custody" still has an asterisk.
Decide this consciously, on its own merits, and record the decision itself — not just the
resulting technical config — in docs/TOKEN_REGISTRY.md §4 once made.
Why cmd/tools/naqd-token deliberately doesn't implement this
The CLI documented on this platform creates mints under the original SPL Token program only — on purpose, not as an oversight:
// This tool creates a mint under the ORIGINAL SPL Token program. That is a
// permanent choice: a mint cannot be migrated to Token-2022 afterwards, and
// Token-2022 extensions (permanent delegate / clawback, confidential
// transfers, transfer hooks for KYC gating) can only be enabled at creation.
// solana-go ships no instruction bindings for those extensions, and
// hand-rolling TLV encoding for a one-time irreversible mainnet action is a
// bad trade — use the official `spl-token` CLI if you want them.
// See docs/TOKEN_REGISTRY.md § "SPL Token vs Token-2022".
Concretely: solana-go (the Go SDK naqd-token is built on) ships the
Token-2022 program ID as a constant, and full instruction-builder bindings for the
original token program — which is what cmdCreate actually calls,
via token.NewInitializeMint2InstructionBuilder — but no equivalent builders for
Token-2022's extension instructions. Enabling permanent delegate or a transfer hook means
hand-encoding TLV (type-length-value) extension data by hand against the raw program, with no
library to check the encoding against. That's a reasonable thing to build and test carefully
for a feature you'll exercise repeatedly; it's the wrong thing to trust for a mainnet mint
creation you get to run exactly once. The official Rust-based spl-token CLI has
first-class, tested support for every Token-2022 extension — use it instead.
# freeze authority only (the conservative, recommended default)
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
create-token --decimals 6 --enable-freeze
# add clawback as well — only once the policy question above is actually decided
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
create-token --decimals 6 --enable-freeze --enable-permanent-delegate
Decimals must stay ≥ 4 — the platform's NAQD settlement exponent (Ledger → On rounding) — or ledger amounts become unrepresentable on chain. 6 is the recommended value and what naqd-token's defaultDecimals constant and every worked example on this site assume, matching USDC's convention.
Why Solana, and when you'd choose otherwise
MyNEX is custodial and ledger-first: a user's NAQD balance is a row in the ledger,
not a wallet UTXO or account, until they explicitly withdraw on-chain
(NAQD → Custodial-first design). Every mint, burn, and
transfer inside the platform posts to the ledger first and calls the chain
second (NAQD Minting → Mint lifecycle) —
which means the chain is not this product's hot path. It's a settlement/proof/self-custody-exit
rail: it exists so circulating supply is publicly checkable against the reserve, and so a
user can leave custodial holding entirely by withdrawing to an address only they control.
That materially lowers the stakes of which chain is primary compared to a wallet-native
token where every user interaction touches the chain directly — and it's exactly why
ChainClient (NAQD Minting → Custody &
the ChainClient interface) is an interface with three interchangeable implementations
rather than Solana-specific code sprinkled through stablecoin-service.
Everything above this callout on this page is verified line-by-line against the code that
runs. This section is different: it's a reasoned comparison of chains for a product still
deciding, using published facts about each network as of 2026 plus the platform's own
declared priorities. Treat it as the current best answer, not something grep
can confirm the way the rest of this site is confirmed.
| Criterion | Solana | Stellar | Base & EVM L2s | Polygon |
|---|---|---|---|---|
| Cost / finality per withdrawal | Sub-cent fees, sub-second finality | ~5s finality, very low fees, native issuance (no smart contract needed) | Low fees, ~2s finality, inherits Ethereum settlement security | Low fees, ~2s finality |
| Wallet + exchange reach for self-custody exit | Broad — largest stablecoin chain outside Ethereum; major wallets and every major exchange | Strong specifically in remittance rails: Circle USDC/EURC issuance, MoneyGram integration | Strong and growing, Coinbase-anchored reach | Broad but has lost mindshare since 2024 |
| Compliance primitives | Token-2022: native freeze, permanent delegate, transfer hooks — being adopted for institutional stablecoins | Native asset controls (authorize/clawback flags) built into the base ledger, not an extension | Standard ERC-20 + optional proxy patterns; nothing as integrated as Token-2022 | Same ERC-20 model as Base/EVM L2s |
| Malaysia-corridor relevance | Solid regional exchange/wallet liquidity across Southeast Asia | Genuinely strong — MoneyGram/remittance rails are widely used in the same migrant-worker corridors MyNEX's Touch 'n Go integration targets | Weaker regional (US/EU-centric) presence today | Was relevant, has faded |
| Operational maturity (2026) | 22+ months without a major outage; Firedancer shipped to mainnet Dec 2025, ending single-client risk, 20%+ validator adoption by Q2 2026 | Mature, low-drama, narrower ecosystem | Maturing quickly, shorter track record than Solana or Stellar | Mature but not where new stablecoin activity concentrates |
Solana as primary is the right call, as the product is scoped today. It's the largest stablecoin venue outside Ethereum, its cost/finality profile is a good match for a self-custody-exit rail rather than a payments hot path, Token-2022 gives the compliance primitives an asset-backed instrument plausibly needs without switching chains, and 2026's operational picture (22+ months incident-free, Firedancer ending single-client risk) closes the biggest historical objection to Solana specifically. Stellar is the alternative genuinely worth respecting here — not Base, not Polygon. Its remittance-native design and existing MoneyGram/Circle rails line up unusually well with the Touch 'n Go (Malaysia) corridor this platform has already committed to as its first partner rail (Data Flows → TnG reload).
What would change this answer: if MyNEX's center of gravity shifted from
"store of value plus a self-custody exit" toward "payments/remittance rail as the primary
use case" — NAQD moving through corridors like Malaysia more than it sits in a wallet as
saved value — Stellar's remittance-native issuance and MoneyGram/Circle integrations would
deserve a serious, product-led re-evaluation, not just a technical one. Separately, a major
Solana outage or a client-diversity regression would have been a real reason to hesitate as
recently as late 2025, before Firedancer shipped; that specific risk is materially smaller
now. Polygon's position is worth naming plainly: it's still the platform's declared secondary
chain in configuration, but polygon.Client.DeployERC20Token is a literal
not-implemented stub today (NAQD Minting → The two
meanings of "minting") — the gap between "declared secondary chain" and "chain with any
working deployment path" is real, not a rounding error.
Operating & rotating
cmd/tools/naqd-token is the operational surface for token creation, inspection,
and break-glass issuance — not the platform's normal issuance path.
| Command | Does |
|---|---|
naqd-token keygen --keypair <path> | Generates a treasury keypair file (mode 0600); refuses to overwrite an existing file |
naqd-token airdrop --keypair <path> --network devnet --sol <n> | Requests devnet/testnet SOL; refuses on mainnet (no faucet exists there) |
naqd-token create --keypair <path> --network <net> | Creates the mint under the original SPL Token program; optional --supply for an initial issuance to the treasury itself |
naqd-token info --mint <addr> --network <net> | Reads decimals, supply, mint/freeze authority straight from the mint account — works against any SPL mint |
naqd-token issue --keypair <path> --mint <addr> --to <addr> --amount <n> | Mints units directly on-chain — see the warning below before ever running this against mainnet |
Every command except info refuses to touch mainnet without --yes-really-mainnet, checked in netFlags.endpoint() — a deliberate second confirmation on top of whatever shell history or muscle memory got the operator there.
In ordinary operation, the platform issues NAQD through
stablecoin-service — the mint worker path in full at
NAQD Minting → Mint lifecycle, which posts a ledger
transaction before ever calling ChainClient.Mint. naqd-token issue
calls the SPL MintTo instruction directly against the chain — it never creates a
NAQDOperation row and never touches the ledger. Anything minted this way is
invisible to ledger.Reconcile (Reconciliation
→ The reconcile tool), because that check only compares each wallet's cached balance
against its own ledger entries — a CLI-issued mint never touches a wallet or the ledger at
all, so there's nothing there to disagree. The only way it surfaces is the manual check
docs/TOKEN_REGISTRY.md §6 already prescribes: run naqd-token info
for the real on-chain supply and compare it by hand against the admin console's NAQD overview
(SumCompletedOperations-derived, per NAQD → Reserve
accounting) — the two should match, and a gap means something minted outside the ledger's
view. This CLI exists for mint creation, inspection, and break-glass situations only.
Authority rotation
| Action | Command | Effect |
|---|---|---|
| Rotate mint authority | spl-token authorize <mint> mint <new-authority> | Old key loses issuance power immediately — record the change in the registry's §1 and §2 the same day |
| Renounce mint authority permanently | spl-token authorize <mint> mint --disable | Fixes total supply forever. Irreversible — no key can ever mint against this address again |
Incident response — suspected key compromise, in order
- Rotate the mint authority first.
spl-token authorize <mint> mint <new-authority>— minutes matter here, since a compromised authority can issue unbacked supply for as long as it stays valid. Do this before investigating, not after. - Freeze suspicious accounts, if a freeze authority exists and holder accounts look affected — a mitigation, not a fix, for the underlying key exposure.
- Audit
naqd_operationsagainst on-chain supply. Every legitimate mint has a corresponding completedNAQDOperationrow; anything minted through the compromised key directly (bypassing stablecoin-service, same mechanism as CLI issuance above) will not. - Reconcile the ledger's
naqd_reserveaccount against both figures — the full procedure is documented at Reconciliation → NAQD reserve accounting.
Which key is compromised changes what's actually at risk — cross-reference against the blast-radius column in the custody diagram above before deciding how urgently to escalate.