A simple scanner gives one of two answers for each address: safe or not safe. Real addresses do not fit that shape.
Take the text 0x followed by 64 hex characters. It can be an account on Sui, Aptos, IOTA or Starknet. A Canton party ID hides a key that only the network operators can see. A Zcash shielded address does not contain the spend key at all. And many people paste a token contract instead of their own wallet address.
Each of these cases breaks a simple rule. In this article, I go through the hard cases in Bunker one by one. For each case, you see the options that I considered, the choice, the code from the project, and a real run of the tool.
Some examples use sample addresses. I built them with the project’s own encoder, in the same way as the Bunker test suite. They are valid formats, and they belong to nobody.
Why “safe or not safe” is the wrong answer
Networks show the public key in different ways. Bunker calls the public key the lock. Some networks show the lock from day one. Others hide it until the first send. On some networks, the answer depends on a pool or an operator.
Decision record ADR-007 compares three ways to report a result:
| Option | For | Against |
|---|---|---|
| Yes or no | Very simple | Wrong. “Hidden until used” is neither yes nor no. It depends on the history of the address. |
| A score from 0 to 100 | One number | It looks exact when it is not. It hides the reason. |
| Named models, plus the answer of the user (chosen) | Each model has a clear rule and a clear action | More categories to explain |
Bunker uses seven named models. Each model maps to one status on the card and one step in the plan:
| Model | Example | Status on the card |
|---|---|---|
visible | Taproot, Solana, Monero | Lock visible |
hidden-until-used | bc1q, EVM, Cosmos | Depends on the answer of the user |
hash-safe | BIP-360 (bc1z) | Hash-protected |
move-pool | Zcash Sapling | Lock visible, with a pool to move to |
ask-provider | Canton, Zcash unified | Ask your provider, or Check your pool for Zcash |
xpub | Extended public keys | Shows every key |
unknown | Typos, contracts, test networks | Not rated |
One design rule keeps this clear. A model describes the network, not the person. The history of the person is a separate input: the answer to one question.
When only you know the answer
Most addresses use the model hidden-until-used. Their result depends on one fact. Has the address ever sent a transaction or signed a message?
A blockchain lookup can find transactions, but Bunker asks the user instead. Decision record ADR-008 gives two reasons.
- Privacy. A lookup sends the addresses to API providers. Each provider then sees a list of addresses that belong to one person.
- Coverage. Off-chain signatures also show the key. A token permit, a “sign in with wallet” message and an NFT listing are examples. No blockchain lookup can find them. Only the user knows.
So each card asks the question, with three answers: Not sure, No and Yes. An explorer link next to the question helps the user find out. “No” gives Lock hidden. “Yes” gives Lock visible. “Not sure” gives Check needed, and the plan adds a step to look for an outgoing transaction on an explorer.
The command-line tool takes the same answers with one option for each address: --sent ADDRESS=ANSWER, where the answer is yes, no or unsure.
One format, four networks
Sui, Aptos and IOTA write addresses as 0x plus 64 hex characters. A Starknet address can have the same shape. The rule for the key is the same on all four networks. The address comes from a hash, so the key stays hidden until the first transaction.
The explorer link and the advice are different, though. Decision record ADR-011 lists the options:
| Option | For | Against |
|---|---|---|
| Guess the most common network | No click | Wrong for many users, with a wrong explorer link |
| Reject the address | Never wrong | No help for the user |
| Show the result and a row of network buttons (chosen) | Always correct after one click | One more control on the card |
The user gets a correct status before the click, because the model is the same for all four networks. The click changes only the network name, the explorer link and the advice.
Bunker also removes a choice when the math excludes it. A Starknet address is a number below 2251. Written with 64 hex characters, that number starts with 0, and the second digit is 0 to 7. In any other case, the number is too large for Starknet, so the Starknet button does not appear. Version 1.2.2 added the check on the second digit (P-44). I scanned four addresses to show this:
| Input | Result type | Network buttons |
|---|---|---|
0x05aaaa...aaaa (64 hex characters) | Move or Starknet account | sui, aptos, iota, starknet |
0x9aaaaa...aaaa (64 hex characters) | Move or Starknet account | sui, aptos, iota |
0x6ff58b...c68a95 (64 hex characters, the IOTA test address) | Move or Starknet account | sui, aptos, iota |
0x2aaaaa...aaaa (63 hex characters) | Starknet account | None. Bunker rates 43 to 63 hex characters as Starknet when the value is below 2251. |
The EVM format is a different case. One 0x address with 40 hex characters works on more than 25 networks, with the same key and the same advice. So the card lists those networks and asks for no click.
Two networks, one prefix
Every Bech32 address starts with a short prefix, such as bc for Bitcoin. Problem report P-03 records a clash. Spark, a Bitcoin layer, used the prefix sp before it moved to spark. Bitcoin Silent Payments (BIP-352) also use sp.
Treating every sp1 address as Spark is simple, but it is wrong for Silent Payments. The fix reads the first data character after the 1. Silent Payments version 0 starts with q. Spark starts with p.
I scanned the BIP-352 test vector and a sample Spark address in the old sp1p form. Bunker told them apart:
| Input | Result type | Explanation from Bunker |
|---|---|---|
sp1qqgste7k9... | Silent Payment address (BIP-352) | A Silent Payment address publishes its scan and spend public keys, so senders can pay it in private. |
sp1pgssyauk... | Spark address | A Spark address contains the identity public key of the wallet, so the lock is visible from day one. |
Both results are Lock visible, but the advice differs. For Spark, the plan says to keep spending money on Spark and savings on a new bc1q address on Bitcoin. A Silent Payment address never appears on chain, so it gets no explorer link.
These two types belong to a wider group: addresses that carry the public key by design. Taproot (bc1p) is another example. A new address of the same type does not hide anything. So the plan puts these addresses in the group “Visible with no hidden option yet”.
Networks where the operator holds the answer
Banks and other institutions use permissioned networks. Only approved parties can join them. Canton, Kinexys (J.P. Morgan) and Partior are examples. On these networks, the public keys are visible to the network operators, not to the whole internet.
| Option | For | Against |
|---|---|---|
| Rate them as visible | Simple | Wrong, because only the operators see the keys |
| Ignore them | Simple | No help for institutions, which hold large values |
| Recognize the format, give no rating, and list the questions for the provider (chosen) | Correct and useful | The user must contact the provider |
A Canton party ID has a public format: a hint, two colons, 1220 and 64 hex characters. I scanned the sample party ID from the Bunker test suite, Alice::1220abab...abab. The card showed Ask your provider, with this explanation:
From the Bunker result:
The party ID ends in a hash of its namespace key. The key itself is shared with the nodes on its synchronizer. Who can see it depends on the network operators.
The plan then put one step in the group “Check or ask”. Here is the exact text of that step:
From the Bunker plan:
Ask your validator, custodian or node operator three questions about this party. Who can read the keys? Can the keys change? Is there a dated plan to move to post-quantum keys?
These are the three questions of decision record ADR-012, word for word. Version 1.2.2 added the third question to the plan (P-45).
Decision record ADR-012 names three questions for any permissioned network, and links them to public standards:
| Question | Why it matters |
|---|---|
| Who can read the keys? | On a permissioned network, the operators see the keys. The provider must say who they are. |
| Can the keys change? | NIST CSWP 39 calls this crypto agility: the ability to change algorithms. |
| Is there a dated plan to move to post-quantum keys? | NIST IR 8547 (draft) plans to deprecate classic public-key algorithms by 2030, and to disallow them by 2035. |
Kinexys and Partior have no public addresses at all. No scanner can examine them, so Bunker lists them as a known limit. They need a review of the provider.
Zcash: rate the pool, not the address
A Zcash shielded address does not contain the spend key. The risk comes from the pool that holds the funds. A pool is a separate part of the Zcash ledger that holds shielded funds. Sapling, Orchard and Ironwood are pools.
Problem report P-07 records the choice. A rating that copies the Bitcoin rule would use the wrong model. No rating at all would give Zcash holders no help. So Bunker rates the pool, with the analysis of the Zcash developers themselves.
That analysis is the ZIP 2005 draft on quantum resilience. It says that a break of the curve math could allow theft or counterfeit in the shielded pools. On July 28, 2026, Zcash activated a new pool, Ironwood (NU6.3). Ironwood supports quantum-recoverable notes. Sapling gets no recovery path.
Here is the rule in src/networks/zcash.js. It runs after the decoder has read the address and checked its checksum:
if (dec.hrp === 'zs') {
return verdict('zec', 'Zcash Sapling shielded address', MODEL.MOVE_POOL,
'Zcash research says that someone who can break its curve math could steal or counterfeit funds in the Sapling pool. Sapling has no recovery path. Zcash plans to stop Sapling if that threat becomes real.',
{ noLink: true, moveText: 'Move shielded funds into the Ironwood pool, which supports quantum-recoverable notes. Use the migration feature of your wallet and start with a small amount.' });
}
if (dec.hrp === 'u') {
return verdict('zec', 'Zcash unified address', MODEL.ASK_PROVIDER,
'A unified address can receive into more than one shielded pool. The risk depends on the pool that holds the funds, not on the address.',
{ noLink: true, pill: 'Check your pool',
askText: 'Open your Zcash wallet and find the pool that holds the balance. Sapling funds have no quantum recovery path, and Orchard now only allows withdrawals. Move both into Ironwood, which supports quantum-recoverable notes.' });
}
Read the code in this order:
dec.hrpis the prefix of the address.zsmeans Sapling.umeans a unified address.verdict()builds the result. Its arguments are the network, the label, the model and the explanation for the user.- A Sapling address gets the model
MODEL.MOVE_POOL. The status is Lock visible, and the plan text inmoveTextpoints to Ironwood. - A unified address gets
MODEL.ASK_PROVIDERwith the card label “Check your pool”. A unified address can receive into several pools, so the address alone cannot decide the risk. noLink: trueremoves the explorer link for both types of shielded address.
I scanned the two sample addresses from the test suite. The Sapling address went to the plan group “Move to new addresses or a safer pool”. The unified address went to “Check or ask”. Transparent Zcash addresses (t1, t3, tex1) follow the same rules as Bitcoin.
This rule depends on research that can change. The decision record says what to do then: update src/networks/zcash.js and the record together.
A token contract is not a wallet
Problem report P-14 starts with a user question about QNT. Many people copy the QNT token contract, 0x4a22...4675, instead of their own wallet address. A contract is a valid address, so the early tool rated it as a wallet.
A contract has no key to protect. Your tokens use the key of the wallet that holds them. Decision record ADR-022 compares three fixes:
| Option | For | Against |
|---|---|---|
| No list | No upkeep | The wrong result for a common error |
| A full token list, with thousands of contracts | Complete | A large file that needs updates. A list that is out of date gives false confidence. |
| A short list of the most common contracts (chosen) | Small, and it solves the common case | Not complete |
The list covers QNT, USDT, USDC, PAXG, XAUT and WBTC on Ethereum, USDT on Tron, and USDC and USDT on Solana. Version 1.2.1 added two Solana programs, because explorers show them next to every transaction. The check is in src/networks/known-contracts.js:
export function detectKnownContract(a) {
const key = /^0x/i.test(a) ? a.toLowerCase() : a;
const hit = Object.prototype.hasOwnProperty.call(KNOWN_CONTRACTS, key) ? KNOWN_CONTRACTS[key] : null;
if (hit) {
return verdict(hit[0], 'Token contract: ' + hit[1], MODEL.UNKNOWN,
'This is the token contract, not a wallet. It has no key to protect. Paste your own address that holds this token.');
}
const prog = Object.prototype.hasOwnProperty.call(KNOWN_PROGRAMS, key) ? KNOWN_PROGRAMS[key] : null;
if (!prog) return null;
return verdict(prog[0], 'Program: ' + prog[1], MODEL.UNKNOWN,
'This is a program, not a wallet. It has no key to protect. Paste your own address.');
}
- The line with
const keymakes a lookup key. An Ethereum address can mix capital and small letters, so the code writes every0xaddress in small letters. Other formats, such as Tron and Solana, keep their exact case, because the case is part of the address. - The code looks for the key in the list of contracts.
hasOwnPropertymakes sure that only real entries of the list count. - A match gets the model
MODEL.UNKNOWN. The result is Not rated, with an instruction to paste the wallet address. - The same steps run for the short list of programs. If nothing matches, the function returns
null, and the next detector gets the address.
This detector runs first in the pipeline. A token contract can look like any wallet address, so no other detector may see it first. I scanned three entries from the list:
| Input | Result type | Status |
|---|---|---|
0x4a220e60...54675 | Token contract: QNT (Quant) | Not rated |
TR7NHqjeKQ...jLj6t | Token contract: USDT on Tron | Not rated |
1111...1111 (32 characters) | Program: Solana System Program | Not rated |
Only links that someone checked
An explorer link helps the user answer “Has it ever sent?”. Each explorer has its own URL pattern, and patterns change. Decision record ADR-014 compares four options:
| Option | For | Against |
|---|---|---|
| A best guess for every network | A link everywhere | Broken links make the tool look careless. A wrong page can mislead. |
| No links | Nothing to break | The user must find an explorer alone. |
| One explorer for all networks | One pattern | No single explorer covers more than 100 networks. |
| Verified links only, with a weekly automatic check (chosen) | Every link works when it ships, and broken links are found fast | Some networks have no link. |
Two problem reports show why guesses are not safe. In P-08, the Cosmos links used the path /address/. A search of real Mintscan pages showed that all of them use /account/. In P-09, the links for Spark and Provenance came from memory, and no indexed page matched them. The fix removed both links.
From docs/06-problems-and-fixes.md, P-09:
Remove them. A missing link is better than a broken link.
The script tools/check-links.js builds one link for each explorer pattern from the test addresses. It opens each link and reports the result. The project’s CI runs it every Monday at 06:17 UTC. It is the only script in the project that uses the network. The page itself never does.
Every link must also pass a safety check. The function explorerUrl() encodes the address and returns the link only if it is a valid https URL. The page sends no referrer, so the explorer does not learn where the user came from.
Some networks have no link by design. Monero is private. Silent Payments and MWEB addresses never appear on chain. Canton is permissioned. Opening a link always sends one address to that explorer, so the user decides. The page never opens a link alone.
Read the decisions
Every case in this article has a full record in the repository, with the context, the options and the consequences:
- Named models: ADR-007
- Ask instead of lookup: ADR-008
- Shared formats: ADR-011
- Permissioned networks: ADR-012
- Zcash pools: ADR-013
- Explorer links: ADR-014
- Token contracts: ADR-022
- The problem reports: docs/06-problems-and-fixes.md
To test the cases yourself, open bunker.nosistech.com and paste an address of each type. The code is at github.com/nosistech/nosis-bunker.
In the next article, I show how AI agents use Bunker. You will see the local command-line tool, the MCP server, and why Bunker has no web API.
Have a question or feedback about Bunker? Contact NosisTech.
Keep reading: Bunker by NosisTech
Previous article: Zero Dependencies: How Bunker Keeps Outside Code Out of the Build
Next article: Bunker for AI Agents: A Local CLI and MCP Server With No Web API
All articles: NosisTech Writing
This post is general information, not financial, legal or security advice. Bunker is an educational tool with no warranty. Examine each address on a block explorer, and send a small test amount before you move funds.