Skip to main content

Tracking a token that is not in the catalogue

ChainOS credits deposits of a fixed, published set of tokens. A deposit of a contract that is not in that set is not detected and not credited — the funds are at the address, they are yours, and nothing in your ledger knows about them.

That is a design choice rather than a limitation to work around. A chain carries thousands of tokens, most of them worthless, and many of them built specifically to resemble one that is not. An allowlist is the only thing standing between your customers' balances and a contract named USDT that somebody deployed this morning.

This page is how you add a real one.

1. Check it is actually absent

curl -s $CHAINOS/v1/treasury/tokens -H "X-API-Key: $CHAINOS_KEY"
curl -s $CHAINOS/v1/treasury/tokens/USDC -H "X-API-Key: $CHAINOS_KEY"
{
"symbol": "USDC",
"deployments": [
{ "chain": "eth", "contract": "0xA0b8…", "decimals": 6, "monitored": true },
{ "chain": "polygon", "contract": "0x3c49…", "decimals": 6, "monitored": true }
],
"unsupportedChains": [
{ "chain": "bsc", "reason": "Circle does not issue native USDC on BNB Smart Chain." }
]
}

Every token response carries unsupportedChains alongside its deployments, each with a reason. A missing pair is always one or the other — listed as deployed, or listed as deliberately absent with an explanation. There is no third category of "unknown", and reading this field while you are designing is how you find a gap at integration time rather than through a 422 in production.

So before you request anything, check three things:

  1. Is the token listed at all?
  2. Is the chain you care about in deployments, or in unsupportedChains?
  3. Is the contract address in the deployment the one you expect?

Point 3 catches more problems than the other two. A token listed on a chain at a contract you did not expect usually means you are looking at a bridged version — see below.

2. Bridged, deprecated, and why Polygon has two USDCs

Two flags on a deployment matter more than they look.

deprecated — superseded, usually by a native issuance, and still monitored. Deposits to a superseded bridged token are real money and are credited. A deprecated deployment is one you should stop quoting to your users, not one you can stop watching.

bridgeable — whether the token can be moved cross-chain. False for anything you could not redeem with its issuer. Bridging a third-party wrapper would hand you an asset nobody is obliged to honour, so the catalogue refuses to treat it as bridgeable however liquid it looks.

Polygon's bridged USDC.e was both, and was carried as a separate catalogue entry until registry v2 withdrew it. That is the shape every superseded wrapper takes: a different contract from the native issuance, trading at a different price under stress, and not bridgeable with CCTP. If you need one carried, request it — and if you accept both, treat them as two assets in your ledger with two symbols. A customer's USDC.e balance is not a USDC balance, and merging them is a decision you cannot unmake.

3. The decimals trap

Read this before you fill in the request form, because it is the field that costs the most.

USDT is six decimals on Ethereum and eighteen on BNB Smart Chain. Same issuer, same brand, same ticker, different precision.

This is why decimals appears on each deployment rather than once per token, and why you must read it from the deployment you are actually crediting.

Getting it wrongThe result
Using Ethereum's 6 for a BSC depositCredit the customer one million-millionth of what they sent
Using BSC's 18 for an Ethereum depositCredit them a trillion times too much

Neither throws. Both are silent, and the second one is discovered by a customer.

Every deposit webhook carries decimals for exactly this reason. Store it per ledger row — see Customer wallets §4 — and never keep a constant in your code.

4. Request the token

curl -X POST $CHAINOS/v1/treasury/token-requests \
-H "X-API-Key: $CHAINOS_KEY" -H "Content-Type: application/json" \
-d '{
"chain": "eth",
"contractAddress": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"symbol": "DAI",
"decimals": 18,
"note": "Requested by three institutional customers for settlement."
}'
A request does not start monitoring

This records a request and puts the contract in front of a ChainOS administrator. It does not begin monitoring, and deposits of the token are not credited while it is under review.

Do not advertise the token to your customers at this point. Funds sent to an address expecting them to appear will not appear, and you will be explaining why for as long as it takes.

The symbol and decimals you send are recorded as your claim, not adopted as fact. The contract is read on chain, its decimals() is compared with what you declared, and a second administrator approves publication before anything changes.

A mismatch between your declaration and the chain is precisely what that review exists to catch — so state the values as you have them, not as you think they ought to be. A request that quietly corrects itself to match the chain would remove the one signal that says you were looking at the wrong contract.

What to put in the request

FieldGet it from
contractAddressThe issuer's own documentation, not a block explorer search, and not a token list
symbolWhatever your ledger will call it. It is a label, not an identity
decimalsYour own reading of the contract, per chain
noteWhy you need it. A named customer or a volume figure moves faster than "completeness"

Source the contract address from the issuer. A block-explorer search for a ticker returns every contract that claims that name, ordered by how much attention they have attracted. That is how a lookalike contract gets into a token list, and a review is a poor place to discover it.

5. Track it

curl -s $CHAINOS/v1/treasury/token-requests -H "X-API-Key: $CHAINOS_KEY"

The field to act on is monitored. It is false in every state but published:

statemonitoredWhat it means
under_reviewfalseWith an administrator
acceptedfalseApproved into a pending change. Still not credited
publishedtrueIn the catalogue. Deposits are now detected and credited
declinedfalseReviewed and declined. outcome carries the reason

accepted is the state that catches people. It reads like success and credits nothing.

Posting the same contract twice is a no-op — your account counts once either way, so a retry after a timeout is safe.

6. When the catalogue changes

Publication is a versioned operation. GET /v1/treasury/routes returns the registryVersion its answer came from, and a new version reaches every node within seconds of being published. You do not need to do anything to pick it up.

A token already in the catalogue never changes its contract address or decimals. A correction is published as a new version — which is auditable — rather than edited in place, which is not.

Deposits that arrived while you were waiting

They are still there. The token was not monitored, so nothing detected them, but the funds sat at an address derived from your mnemonic the whole time.

Once the token is published, monitoring begins from that point. Whether earlier transfers are picked up depends on the chain and how far back the ingest path looks, so treat a pre-publication deposit as something you reconcile by hand: find it on the chain, record it in your ledger with the right decimals, and match it to a customer from the address.

The lesson is the ordering. Request the token, wait for published, then tell customers they may send it.

7. After it is published

Three things need doing, and none happens automatically.

Add it to your own asset table, with the per-chain contract and per-chain decimals. Your ledger needs to know it exists before a webhook arrives mentioning it.

Create a sweep policy for it. Sweep policies are per (chain, token). A newly published token has no policy, so its deposits accumulate at customer addresses until you configure one. Preview it first and set the dustFloor from this token's economics rather than copying the one beside it. See Sweeps.

Check whether it can bridge. A token with bridgeable: false cannot be moved cross-chain by ChainOS, whatever liquidity exists for it elsewhere. If your treasury design assumed consolidating it onto one chain, check this before the design depends on it:

GET /v1/treasury/routes/check?token=DAI&from=eth&to=polygon

Most tokens outside the issuer-operated set are not bridgeable here, and that is the expected answer rather than a gap.

8. What is not built

Per-account token enablement does not exist. The catalogue is platform-wide: a published token is published for everybody, and there is no way to enable a token for your account alone or to opt out of one you do not want to credit.

In practice that means you should filter on your side. If a customer deposits a catalogue token you do not offer, your webhook handler will receive it — decide deliberately whether to credit it, hold it unattributed, or return it, rather than letting a default branch credit an asset you never agreed to hold.

Next