Tok{In} API
The Tok{In} API offers an array of features that can help determine whether a token is malicious or not. From storage checks, to simulation and static analysis features. The Tok{In} API supports all ERC20 contracts on Ethereum, Binance Smart Chain, Arbitrum, Base, Avalanche, and Robinhood Chain (New). The features cover all Uniswap V2/V3/V4 forks, Aerodrome V2/Slipstream (Base), and launchpad tokens: both bonding curves (four.meme and flap.sh on BSC, trench.today and flap on Robinhood Chain) and launchpads that seed a standard DEX pool directly (Pons, Noxa, Virtuals, Doppler, varo.rialto, bow.fun and letscash.fun on Robinhood Chain).
For AI coding agents (Claude Code, Cursor, Copilot, etc.): a migration guide written specifically for you lives at
AGENTS.mdalongside this page (canonical URL:https://docs.dedaub.com/docs/token_safety/AGENTS). Read it end-to-end before changing any code that integrates with Tok{In}: it covers the GoPlus → Tok{In} drop-in swap, field mappings, always-nullfields, and common migration pitfalls.
Usage
curl -X 'GET' \
'https://tokin-api.dedaub.com/token/{chain}/{token_address}' \
-H 'accept: application/json' \
-H 'X-API-Key: YOUR-API-KEY'
chain: The slug or the EVM chain id of the chain (see Available Chains)
token_address: The address of the token
Optional query parameters:
| Parameter | Default | Effect |
|---|---|---|
response_format | dedaub | goplus gives the GoPlus-compatible shape. See Response Formats. |
include_token_price | false | true fills the four price fields. Without it they are null. See Price fields. |
refresh | false | true computes the state part of the analysis again (pools, state features, source analysis). It needs an enterprise plan or an admin key. If not, the API answers 403. |
redecompile | false | true runs the decompiler again. It needs an enterprise plan or an admin key. If not, the API answers 403. |
User Interface
A Swagger UI for easier interaction with the API is available at https://tokin-api.dedaub.com . The field descriptions in the Swagger UI are short. This page is the full reference.
Available Chains
| Chain | Slug | Chain ID |
|---|---|---|
| Ethereum | ethereum | 1 |
| Binance Smart Chain | binance | 56 |
| Arbitrum | arbitrum | 42161 |
| Base | base | 8453 |
| Avalanche | avalanche | 43114 |
| Robinhood Chain (New) | robinhood | 4663 |
DEX coverage is most complete on Ethereum, with more pools being added to the other chains over time.
Quote tokens per chain
Pool discovery and liquidity valuation are anchored on each chain's wrapped native token plus its canonical ~$1 stablecoins. That anchor set is not the same on every chain:
| Chain | Wrapped native | Stable anchors |
|---|---|---|
| Ethereum, Arbitrum, Base | WETH | USDC, USDT |
| Binance Smart Chain | WBNB | USDC, USDT |
| Avalanche | WAVAX | USDC, USDT |
| Robinhood Chain | WETH | USDG, USDE |
Robinhood Chain has no canonical USDC or USDT: USDG and USDE are the real ~$1 tokens there, and any USDC/USDT-named deployment on 4663 is an impostor. Pools quoted in a non-anchor token can still be valued when a price is available (see Notes on liquidity).
Response Formats
The Tok{In} API supports multiple response formats via the response_format query parameter.
Available Formats
| Format | Description |
|---|---|
dedaub | Default format |
goplus | GoPlus-compatible format |
Usage
# Default (Dedaub format)
GET /token/{chain}/{token_address}
# GoPlus format
GET /token/{chain}/{token_address}?response_format=goplus
GoPlus Format Mapping
The GoPlus format transforms the default Tok{In} schema to a GoPlus-compatible one, with the following caveats.
Direct Mappings
| Dedaub Field | GoPlus Field | Notes |
|---|---|---|
is_open_source | is_open_source | Boolean → "1"/"0" string |
is_proxy | is_proxy | Boolean → "1"/"0" string |
mint_or_burn_function | is_mintable | Boolean → "1"/"0" string |
owner_address | owner_address | Direct string. Not a renouncement signal, see caveat 7 |
can_selfdestruct | selfdestruct | Boolean → "1"/"0" string |
external_call | external_call | Boolean → "1"/"0" string |
is_in_dex | is_in_dex | Boolean → "1"/"0" string |
receive_tax | buy_tax | Percentage as string ("1" is 1%); pool-aware, see caveat 4; scale, see caveat 8 |
send_tax | sell_tax | Percentage as string ("1" is 1%); pool-aware, see caveat 4; scale, see caveat 8 |
cannot_buy | cannot_buy | Boolean → "1"/"0" string |
tax_can_be_modified | slippage_modifiable | Boolean → "1"/"0" string. Not owner-gated |
trading_cooldown | trading_cooldown | Boolean → "1"/"0" string |
creator_address | creator_address | Direct string |
creator_percent | creator_percent | Percentage as string ("0.02" is 0.02%); scale, see caveat 8 |
is_launchpad_token | launchpad_token | Boolean → "1"/"0" string |
Computed Mappings
| GoPlus Field | Derived From | Logic |
|---|---|---|
transfer_pausable | cannot_buy, pause_status_can_be_modified | True if either is True. Reads pause_status_can_be_modified after the owner gate |
is_blacklisted | has_blacklist_or_whitelist | Same value (temporary) |
is_whitelisted | has_blacklist_or_whitelist | Same value (temporary) |
is_anti_whale | has_trading_cap, has_position_cap | True if either is True |
anti_whale_modifiable | trading_cap_can_be_modified, position_cap_can_be_modified | True if either is True. Reads both flags after the owner gate |
hidden_owner | allowance_bypass, privileged_spenders | True when the bypass is address specific, so at least one address is named |
owner_change_balance | owner_can_change_balance | Same value. null while the owner simulation is off. It is off today |
Fields Without Direct Equivalent (returns null)
| GoPlus Field | Reason |
|---|---|
cannot_sell_all | Not currently tracked |
can_take_back_ownership | Not currently tracked |
gas_abuse | Not currently tracked |
personal_slippage_modifiable | Not currently tracked |
lp_total_supply | Not currently tracked |
is_airdrop_scam | Not currently tracked |
trust_list | Not currently tracked |
holder_count | Not currently tracked |
owner_balance | Not currently tracked |
owner_percent | Not currently tracked |
Caveats
-
is_blacklisted/is_whitelisted: Currently both map fromhas_blacklist_or_whitelist. We cannot distinguish between blacklist-only, whitelist-only, or both. -
Boolean Format: GoPlus uses string format (
"1"/"0") instead of booleans. All boolean fields are converted accordingly. -
Tax Terminology: Our
send_taxmaps to GoPlussell_tax, and ourreceive_taxmaps to GoPlusbuy_tax. Note the direction inverts: code computing slippage must follow the rename. -
buy_tax/sell_taxare pool-aware, and may disagree with the contract-level tax. Ingoplusmode these are taken from the first pool index[]that reports a tax, falling back to the contract-levelreceive_tax/send_taxonly when no pool reports one. The pool figure reflects what a real swap would cost and is usually the more useful number, but it meansbuy_taxin agoplusresponse can differ fromreceive_taxin the equivalentdedaubresponse for the same token. If you compare the two formats, expect this and prefer the pool figure for trade decisions. -
is_honeypotis alwaysnull. It is present in the response shape but not yet produced. Integrations that gate trades onis_honeypot === "1"will treat every token as safe after migrating. Derive an equivalent fromcannot_buy, the per-pooldex[].buy_success/sell_success(dedaubmode), and the tax fields; or block while the field is unavailable. See Readingbuy_success/sell_successfirst, because thenull/"pending"states matter for exactly this decision. -
hidden_owneris narrower than it looks: it is"1"only when the bypass is address-specific. A universal bypass (where the allowance check is skipped for everyone) setsallowance_bypassbut leavesprivileged_spendersempty, and maps to"0"because no undisclosed owner exists. It isnullwhen the check could not run, which is not the same as"0". Use the Dedaub format if you need to tell those three apart, or to read the offending addresses. -
No GoPlus field carries the ownership verdict or the timebomb result.
ownership_renounced,timebomb_statusandtimebomb_deadlinesare only in thededaubformat. Do not useowner_addressto decide if ownership is renounced. It is the answer of an owner getter, and it isnullwhen no getter answered. In that case the verdict can be "renounced" or "not determined" (seeownership_renounced).transfer_pausableandanti_whale_modifiableread the setter flags after the owner gate. So"0"can mean that the code has a setter, but the owner is gone.slippage_modifiableis not owner-gated. -
buy_tax,sell_taxandcreator_percentare percentages."1"is 1%. The GoPlus API uses a 0–1 scale for these fields, where"0.01"is 1%. A client that reads these values on the GoPlus scale gets a value that is 100 times too high.holders[].percentuses the 0–1 scale of GoPlus. The Swagger UI text for these fields says 0–1 today. That text is wrong. Use the scale on this page.
Static Analysis Features
We leverage the Gigahorse decompiler to analyze contracts whose source code is unavailable. The decompiled bytecode is checked for patterns commonly associated with undesirable behaviors in ERC20 tokens.
mint_or_burn_function:
There is a public function that changes the token's total supply.
can_selfdestruct:
The contract contains a selfdestruct call. (Note: The EVM no longer supports the SELFDESTRUCT opcode, so this is now a no-op.)
transfer_guarded_by_storage:
The transfer function's execution depends on a storage variable (e.g., a flag or mapping check). This is a signal that transfers can be conditionally blocked by the contract owner or via some internal logic.
transfer_from_guarded_by_storage:
The transferFrom function's execution depends on a storage variable. Same implications as transfer_guarded_by_storage: it indicates that delegated transfers can be conditionally blocked.
trading_cooldown:
An address must wait for a certain period after its last transfer before it can transfer tokens again.
view_function_modifies_state:
A function with the signature of one of the view functions defined by EIP-20 updates a storage variable.
has_trading_cap:
The amount of tokens transferred by a single call to transfer cannot exceed a value, either constant or held in storage.
has_trading_cap_functionality:
There is code that could enforce a trading cap. This can be true even if the cap is disabled.
trading_cap_can_be_modified:
The transfer amount cap is held in storage and there is a function that can update it. The API sends false when ownership_renounced is true. When ownership_renounced is false or null, the flag keeps the static result. See Owner-gated setters.
has_position_cap:
The amount of tokens an address can hold cannot exceed a value, either constant or held in storage.
has_position_cap_functionality:
There is code that could enforce a position cap. This can be true even if the cap is disabled.
position_cap_can_be_modified:
The position cap is held in storage and there is a function that can update it. The API sends false when ownership_renounced is true. When ownership_renounced is false or null, the flag keeps the static result. See Owner-gated setters.
has_owner_functionality:
There is functionality that could let one or more addresses have access to functionality of the token that the rest do not. This can be true even if the owner has been renounced. For the current owner state, read ownership_renounced.
owner_address:
The address that an owner getter gives: owner(), getOwner() or _owner(). The API calls the getters at the token address. For a proxy, the call goes through the proxy to the implementation code. When two getters answer, a live address comes before a burn address. null when no getter answered.
Do not use this field to decide if ownership is renounced. Read ownership_renounced.
ownership_renounced:
The ownership verdict of the API.
| Value | Meaning |
|---|---|
true | Renounced. An owner getter gave a burn address. Or no owner getter answered, and the API did not find a wallet in a storage slot that the code uses for its owner checks. The API also gives true when it did not read that slot: there is no analysis, the code keeps its state at a different address, the slot is not a plain address slot, the read failed, or the read took more than 2 seconds. |
false | Live. An owner getter gave an address that is not a burn address. |
null | Not determined. No owner getter answered, but a wallet is in a storage slot that the code uses for its owner checks. Show it as unknown, never as renounced. |
true with owner_address: null means that no owner getter answered. A contract with no owner getter can still give privileges in a different way. Examples are roles, or a contract address in storage, such as a multisig or a timelock. A contract in an owner slot does not change the verdict to null. A slot that the API did not read also leaves true. So in this case true does not prove that nobody has privileges. Read has_owner_functionality too.
When the value is true, the API sends false for three setter flags. See Owner-gated setters.
Since 2026-10-03, the API reads the owner of a proxy through the proxy, and a wallet in an owner slot gives null. Before that date, some proxies (for example USDC and BUSD on Ethereum) and some tokens with a wallet in an owner slot showed true.
⚠ Do not calculate this value from owner_address. The verdict also reads storage, so the two fields can disagree. For example, owner_address is null both when the verdict is true and when it is null.
⚠ Test the key, then the value. null is an answer: "not determined". A response that the API sent before 2026-08-24 does not have this key. Use a fallback only when the key is absent.
has_pause_functionality:
The token has code that could pause trading. Can be true even if the current state of the contract does not allow pausing functionality to be re-enabled.
pause_status_can_be_modified:
The pause status can be changed in the future. The API sends false when ownership_renounced is true. When ownership_renounced is false or null, the flag keeps the static result. See Owner-gated setters.
has_blacklist_or_whitelist:
Addresses can be restricted from trading the token.
external_call:
There is a call to another contract, whose target is not statically known and/or returns arbitrary data that are used by the token.
timebomb:
The code compares a value with block.number or block.timestamp, so some behaviour can start or stop when the chain clock gets to that value. The API places each value against the chain clock and gives the result in timebomb_status and timebomb_deadlines.
timebomb is false when timebomb_status is passed or unreachable, also when the code has the check. Every other status keeps the static finding. A status never changes timebomb from false to true. "pending" means that the analysis has not run.
timebomb_status:
Where the time checks of the code stand now. The API calculates this value each time it builds a response, not at last_scanned_at. A response can come from a cache that is at most two minutes old. So the value can change with time, also when no transaction occurs.
| Value | Meaning | timebomb |
|---|---|---|
armed | A deadline is still ahead. The behaviour of the contract can change when the chain gets to it. | true |
passed | Every deadline has passed, and nobody can move a deadline. Its effect is now the current behaviour, and the other checks measure it. | false |
resettable | No deadline is ahead, but a value in storage can still change. The owner is live or not determined. Or a storage slot for the owner checks holds a value other than zero or a burn address, or the API could not read it. A storage value that is out of reach also gives this status while the owner is not gone. It is a deadline that nobody set yet. | true |
unreachable | The code compares the clock only with limits that no clock gets to, for example the overflow limit 2^32 or the uint48 maximum. These limits are not timers. A storage value at such a limit also gives this status, but only when the owner is gone. | false |
unknown | The API could not place a value against the clock. Or a deadline in storage has passed, but it is not old enough yet (see below). Also when the chain did not answer in 5 seconds. | true |
null | There is no timebomb finding, or the analysis has not run. | false or "pending" |
When the values of one token point to different statuses, the API gives the first status that applies, in this order: armed, unknown, resettable, passed, unreachable. For example, the owner of BAT is not determined, but the API could not place its values, so the status is unknown.
How the API places a value:
- It reads the clock that the contract reads. It runs
block.numberandblock.timestampin a call, and does not read a block header. - A value in the range of Unix time is a timestamp.
- A block number counts only from about two years before the current block to ten years after it. It must also not be a multiple of 60. A duration such as
2592000(30 days) looks like a block number, so an older value or a multiple of 60 is not placed. - A value that is more than ten years ahead of both clocks is a limit (
unreachable). - Other values, for example zero, a small number, a duration or an old block number, are not placed. They give
unknown, unless a deadline is still ahead (armed). - A deadline in storage counts as passed only when the owner is gone and the deadline is old enough. A timestamp must be more than one year old. For a block, the API counts the age at half the nominal block time. So the block must be about two years old by the nominal block time. The code can add a time to the stored value that the analysis does not see.
Live examples on Ethereum, 2026-10-03:
| Token | timebomb_status | timebomb | Why |
|---|---|---|---|
TORN 0x77777feddddffc19ff86db637967013e6c6a116c | passed | false | The bytecode compares the clock with the constant timestamp 1612148437 (2021-02-01). It has passed, and it cannot change. |
COMP 0xc00e94cb662c3520282e6f5717214004a7f26888 | unreachable | false | The code compares the clock only with 0x100000000 (2^32). |
UNI 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 | resettable | true | The stored deadline in slot 0x2 (2024-01-01) has passed. The minter address in slot 0x1 can still write a new one. |
BAT 0x0d8775f648430679a709e98d2b0cb6250d2887ef | unknown | true | The code compares block.number with two stored block numbers from 2017. A block number that old is not placed. |
timebomb_deadlines:
The blocks and times that the code compares with its clock, where the API could place them. The list is in the order of estimated_unix_time.
| Field | Meaning |
|---|---|
source | constant: the value is in the bytecode and cannot change. storage: the value comes from a storage slot. |
slot | The storage slot of a storage value. null for a constant. |
clock | block (a block number) or timestamp (Unix seconds). |
value | The block number or the Unix timestamp. |
passed | true when the clock is at or after value. |
estimated_unix_time | When the clock gets to value, in Unix seconds. Exact for a timestamp. For a block, an estimate from the nominal block time of the chain. |
A limit (unreachable) and a value that the API could not place are not in the list. So the list can be empty, also when timebomb_status is unreachable or unknown. A deadline with passed: true does not turn timebomb off by itself. Read timebomb_status.
⚠ estimated_unix_time of a block is only an estimate. It can be wrong by a large amount, and it changes between two responses. On Binance Smart Chain it can move by minutes, because the real block time is shorter than the nominal block time. For a decision, use passed, which compares with the current block and is exact.
⚠ Arbitrum and Robinhood Chain. On these chains, block.number in a contract gives the Ethereum block number. So a block deadline is an Ethereum block, and its estimated_unix_time uses the Ethereum block time. Do not compare it with the L2 block number in an explorer.
UNI on Ethereum, live on 2026-10-03:
"ownership_renounced": true,
"timebomb": true,
"timebomb_status": "resettable",
"timebomb_deadlines": [
{
"source": "storage",
"slot": "0x2",
"clock": "timestamp",
"value": 1704067200,
"passed": true,
"estimated_unix_time": 1704067200
}
]
The deadline has passed, but timebomb stays true. UNI has no owner getter and its minter is a contract, so ownership_renounced is true. But slot 0x1 holds the minter address, so the stored deadline can still change, and the status is resettable.
has_tax_functionality:
This token has a tax functionality implemented. This could be true even if the current tax is 0 as the taxes could have been wiped.
tax_can_be_modified:
Tax value can be updated. This flag is not owner-gated: the API sends the static result also when ownership_renounced is true. See Owner-gated setters.
is_open_source:
This indicates whether the contract's source code is available.
has_open_source_implementation:
For proxy contracts: indicates whether the implementation contract's source code is verified. Only relevant when is_proxy is true. null for non-proxy contracts.
is_proxy:
Indicates if the contract is a proxy contract.
balance_update_function:
A list of function selectors (e.g., "0xa9059cbb") that modify the token's balance storage slot. Expected selectors are transfer and transferFrom; additional selectors may indicate hidden mint/burn or fee-skimming logic. The list can hold the same selector more than once.
"balance_update_function": [
"0xa9059cbb",
"0x23b872dd"
]
allowance_update_function:
A list of function selectors that modify the token's allowance storage slot. Expected selectors include approve, transferFrom, increaseAllowance, and decreaseAllowance; unexpected selectors may indicate allowance manipulation.
"allowance_update_function": [
"0x095ea7b3",
"0x23b872dd",
"0x39509351",
"0xa457c2d7"
]
Owner-gated setters
Three setter flags tell you that a function can change a limit: pause_status_can_be_modified, trading_cap_can_be_modified and position_cap_can_be_modified. The API assumes that only the owner can call these setters. The analysis does not check the caller.
When ownership_renounced is true, the API sends false for the three flags. The code still has the mechanism: has_pause_functionality, has_trading_cap_functionality and has_position_cap_functionality do not change. When ownership_renounced is false or null, each flag keeps the static result. A flag that is "pending" or null stays as it is.
The gate does not look at a proxy admin. For a proxy, an upgrade admin can still change the code, also when ownership_renounced is true.
tax_can_be_modified is not in this group. In a sample of renounced tokens with a tax setter, about one token in three still had a live address that controls that setter. So a renounced token can still show tax_can_be_modified: true.
REN on Ethereum, live on 2026-10-03:
"owner_address": "0x0000000000000000000000000000000000000001",
"ownership_renounced": true,
"has_pause_functionality": true,
"pause_status_can_be_modified": false
The owner getter gives 0x…0001, which is a burn address. So ownership_renounced is true, and the API sends false for the three setter flags. The code still has the pause mechanism.
Simulation Features
transfer_success:
Whether a holder's balance can actually be moved. false means the transfer did not go through, so the token may block transfers entirely, which is a common honeypot trait. null means the check did not settle, carries no signal, and must not be read as a pass.
The check sends tokens from a holder wallet that the API picks, or from a test wallet that it funds. If the API finds no holder wallet and cannot make a test wallet, transfer_success, send_tax and receive_tax are null. The API does not use the owner wallet in place of a holder wallet. It also does not repeat a failed transfer from the owner wallet. An owner is often exempt from the tax and the limits, so its result is not a valid answer for a holder. Since 2026-10-03, these three fields are null more often for this reason. When the API had no wallet for the check, it does the check again on a request that comes more than 6 hours after the last check. So a null from this cause can change to an answer. Ask again later.
allowance_bypass:
true when some caller can move another holder's tokens through transferFrom without ever having been approved, the shape of a hidden drain backdoor.
The check is behavioural rather than a pattern match on the code, because a backdoor address is rarely a plain literal in a contract. Read it together with privileged_spenders, which is what separates the two positive cases:
allowance_bypass | privileged_spenders | Meaning |
|---|---|---|
true | one or more addresses | Address specific bypass. Those callers can move any holder's balance with no approval in place |
true | empty | Universal bypass. The token does not enforce allowances at all |
false | empty | Allowances are enforced |
null | empty | The check did not run, for example because no holder wallet was available. Unknown, and never safe |
⚠ Tri-state. null means the check could not run and must not be rendered as safe. Only false is an all-clear.
privileged_spenders:
The addresses that can move a holder's balance with no allowance in place. Empty when allowance_bypass is false, and also when the bypass is universal rather than address specific, so read it together with allowance_bypass, never on its own.
"allowance_bypass": true,
"privileged_spenders": [
"0x961c7a5dde069aa1a4ea73e5b506e4312105f08d"
]
funds_can_be_drained:
Somebody other than the holder can move tokens out of the wallet of a holder. This is the strongest finding of the API.
| Value | Meaning |
|---|---|
true | A measurement showed it |
false | At least one check ran and found nothing |
null | No check could run. Also null when a check found nothing, but the code has the shape of an allowance bypass: the API does not publish that negative |
null is not false. Show it as unanswered, never as a pass. allowance_bypass and privileged_spenders tell you the mechanism and the address.
transfer_tax:
send_tax(%): Calculated as the percentage loss from the sender's balance. For example, if the sender sent 100 tokens and 110 were deducted, the send tax is 10%.receive_tax(%): Calculated as the percentage the recipient received. For example, if the sender sent 100 tokens and the recipient only received 90, the receive tax is 10%.- All values are expressed as percentages (e.g., 0.0 means 0%, and 1 means 1%).
nullmeans that the API has no tax figure: the transfer check did not run, or the transfer did not go through (seetransfer_success).nullis not 0%.
dex:
An array of liquidity pools where the token is paired with one of the chain's quote tokens (the wrapped native or a canonical stablecoin), along with details on liquidity and trading behavior. Launchpad bonding curves are reported here too, as synthetic pools.
{
"pair": "0x...",
"liquidity_type": "UniV2",
"liquidity": 2455608.22,
"buy_success": true,
"buy_tax": 0.0,
"sell_success": true,
"sell_tax": 0.0,
"errorMessage": null,
"errorCode": null,
"pool_id": null,
"is_launchpad": false
}
-
pair: Address of the liquidity pair. -
liquidity_type: Type of the pool:Type Description UniV2Uniswap V2 or compatible fork UniV3Uniswap V3 or compatible fork UniV4Uniswap V4 or compatible fork SolidlyAerodrome (more Solidly forks coming!) SolidlyV3Aerodrome Slipstream four.memeFour.Meme bonding curve (BSC) flapFlap.sh bonding curve (BSC, Robinhood Chain) trench.todayTrench bonding curve (Robinhood Chain) The last three are bonding curves rather than DEX pools; they always carry
is_launchpad: true.Uniswap V4 pools are simulated like V2 and V3.
buy_success,sell_success,buy_taxandsell_taxare populated forUniV4pools, including pools quoted in the chain's native coin and pools sitting behind a custom hook. Earlier releases returned no buy/sell result for V4 and reported every V4 pool as 0% tax; a client that cached those values, or that special-cased V4 as "tax unknown, assume zero", should stop doing so. A hook that taxes a swap now shows up inbuy_tax/sell_taxlike any other tax. -
liquidity: USD value of the pool's known-token reserves:- Measured on the known token side. It is an approximation rather than an exact figure, and it is a reliable indicator of pool health and liquidity status. See Notes on
liquidityfor the per version caveats.
- Measured on the known token side. It is an approximation rather than an exact figure, and it is a reliable indicator of pool health and liquidity status. See Notes on
-
buy_success: Whether buying the token (e.g., WETH -> token) succeeded. Four-valued; see below. -
buy_tax(%): Tax incurred on a buy.nullor"pending"on the same terms asbuy_success. -
sell_success: Whether selling the token succeeded. Four-valued, same asbuy_success. -
sell_tax(%): Tax incurred on a sell. -
errorMessage/errorCode: Populated for failed queries (e.g., low liquidity, failed swaps). -
pool_id: ID of the pool in case of Uniswap V4, otherwisenull. -
is_launchpad:trueif this pool is from a launchpad bonding curve (e.g., four.meme, flap.sh, trench.today). Otherwisefalse, ornullin the response that computed the pools. Testis_launchpad === true. Do not test=== false.
Reading buy_success / sell_success: null is not a failure
These four fields are not booleans. Each carries one of four values, and conflating the last two with false is the most common way to misread a Tok{In} response:
| Value | Meaning | Safe reading |
|---|---|---|
true | Simulated, and the trade succeeded | Tradeable on this pool |
false | Simulated, and the trade failed | A real negative signal |
"pending" | Simulation is still running | Not yet known; poll again |
null | No simulation was attempted for this pool | Unknown, carries no signal |
null occurs where the API did not run a swap simulation. Bonding-curve pools are the common case, because a curve is not a DEX pool. The API also does not simulate a pool with less than $100 of liquidity (errorCode: "liquidity_too_low"), a pool with no liquidity at the current price ("no_in_range_liquidity"), or a pool that it cannot address ("missing_swap_parameters", "pair_not_found").
⚠ Do not render null or "pending" as "cannot buy" or "cannot sell". A client that treats these as falsy will report a healthy token as unsellable. Branch on === true and === false explicitly and handle the other two as "unknown". If you need a single tradeability verdict, require sell_success === false before calling a token unsellable, not merely "not true".
Because these fields are typed as boolean-or-string, statically typed clients should model them as a union (e.g. bool | "pending" | None) rather than bool.
is_liquid:
This boolean indicates if the token is liquid, i.e. if at least one pool has more than $100 of value in it, measured on the chain's quote token side: WETH/USDC/USDT on most chains, WETH/USDG/USDE on Robinhood Chain.
Notes on liquidity
- For V2 pools, using the known token side (e.g., WETH in WETH/XYZ) is reliable since the reserves are fixed.
- For V3 pools, the figure is measured on the known token side and is an approximation. It can include uncollected fees and dust, and it does not account for the active price range. It remains a strong signal of pool activity and health, and a very low value is a reliable indicator of an illiquid or abandoned pool.
- For V4 pools, no per pool balance exists to read: every token balance is held by a single
PoolManagercontract rather than by one contract per pool. Liquidity for these pools is sourced from external market data for the pool id, as base and quote liquidity in USD. - For launchpad bonding curve pools (four.meme, flap.sh, trench.today), liquidity is derived from the on-chain reserve data of the bonding curve contract, converted to USD using the quote token price. Curves that quote in the chain's native coin rather than naming a quote token (trench.today, and flap on Robinhood Chain) are valued against the wrapped native.
- When neither side of a pool is a chain anchor, it can still be valued when an external price is available for one side. This is what makes Robinhood Chain's tokenised equities usable as quote assets: a launchpad pool quoted in
COIN,TSLA,GOOGLorSPCXcarries real, reportable liquidity. A pool with no priced side at all is dropped rather than reported at $0.
Launchpad Detection
Tok{In} detects tokens issued by launchpad platforms and reports which platform issued them, on every supported chain.
| Platform | Slug | Chains |
|---|---|---|
| Ape.store | ape.store | Ethereum, BSC, Base, Robinhood Chain |
| Arena | arena | Avalanche, Robinhood Chain |
| ArrowPad | arrowpad | Robinhood Chain |
| Bags | bags | Robinhood Chain |
| bow.fun | bow.fun | Robinhood Chain |
| Circus | circus | Robinhood Chain |
| Clanker | clanker | Ethereum, BSC, Base, Arbitrum, Robinhood Chain |
| Coinbarrel | coinbarrel | Robinhood Chain |
| Doppler (bankr, long.xyz) | doppler | Base, Robinhood Chain |
| DYOR Fun | dyor.fun | Base, Robinhood Chain |
| Flap.sh (bonding curve) | flap | BSC, Base, Robinhood Chain |
| Four.Meme (bonding curve) | four.meme | BSC, Base, Arbitrum |
| HoloLaunch (Holoworld) | hololaunch | BSC, Robinhood Chain |
| hood.fun | hood.fun | Robinhood Chain |
| hyper.meme | hyper.meme | Robinhood Chain |
| Klik | klik | Ethereum, BSC, Robinhood Chain |
| LaunchHood | launchhood | Robinhood Chain |
| LeaveHood | leavehood | Robinhood Chain |
| Lemon.fun | lemon.fun | Robinhood Chain |
| letscash.fun | letscash.fun | Robinhood Chain |
| Livo | livo | Ethereum, Robinhood Chain |
| Lunch.fun | lunch.fun | Robinhood Chain |
| Memecoin.fun | memecoin.fun | Robinhood Chain |
| MintFast | mintfast | Robinhood Chain |
| Noxa | noxa | Robinhood Chain |
| o1.exchange | o1.exchange | Robinhood Chain |
| ORO | oro | Robinhood Chain |
| Pew.fun | pew.fun | Robinhood Chain |
| Pmav.fun | pmav.fun | Robinhood Chain |
| Pons | pons | Robinhood Chain |
| Pons (unofficial fork) | pons-fork | Robinhood Chain |
| Potato.fm | potato.fm | Robinhood Chain |
| Real.fun | real.fun | Robinhood Chain |
| Robinfun | robinfun | Robinhood Chain |
| Stoxes | stoxes.fun | Robinhood Chain |
| Sushi | sushi | BSC, Robinhood Chain |
| Trench (bonding curve) | trench.today | Robinhood Chain |
| varo.rialto | varo.rialto | Robinhood Chain |
| Virtuals Protocol | virtuals | Base, Robinhood Chain |
| Unrecognised launchpad | launchpad | All chains |
Notes on reading the slugs:
- One slug per platform, not per product line. Where a platform runs several launch routes (Pons V1/V2, Stoxes and its RWA line, o1's crypto- and stock-quoted routes, DYOR Fun's versions), they all report the platform's single slug. Screeners that list those routes separately will show more entries than this table does.
pons-forkis an independent operator running the same Pons launcher template. Its tokens look like official Pons tokens, but the launcher belongs to someone else, so it is labelled separately: launchpad reputation is the point of the field, and conflating the two would defeat it.launchpadis a deliberate catch-all. A token that proves it came off a launchpad but whose platform we cannot yet name is still reported as launchpad-issued rather than dropped. Treat it as "yes, launchpad; platform unknown", not as a specific platform.- A platform on one chain is not the same product on another. Ape.store, Clanker, Arena and Virtuals are native to Base, Avalanche or Ethereum and also deployed on Robinhood Chain; the slug is the same, the deployment is not.
Detection is exact: it identifies the contract that issued the token, rather than guessing from the token's address. The one exception is documented below.
Graduated tokens (BSC). Four.Meme and Flap tokens carry a recognisable address pattern. It is used as a last resort on BSC only, for tokens that have left their bonding curve for a standard DEX such as PancakeSwap. Being a pattern match rather than an exact one, it can occasionally label a token that matches by coincidence. The exact signals are always preferred when available.
Bonding curves appear as pools. Four.Meme, Flap and Trench hold their reserves in a curve contract, so a token still on its curve appears in the dex array with liquidity_type set to the platform slug and is_launchpad: true. Every other platform in the table seeds a standard Uniswap pool at launch, so its tokens carry is_launchpad_token: true while their pools look like ordinary V2/V3/V4 pools.
How a platform is identified
The evidence is always the contract that issued the token. For a curve platform that is the curve contract itself. For everything else the pool is an ordinary Uniswap pool that pair discovery already finds, so only the label is missing, and it is resolved from the issuer behind that pool. Several signals supply it and they are tried cheapest first, so most tokens are labelled without an extra RPC call.
Four properties of the result matter when you consume the field:
| Property | What it means for your integration |
|---|---|
| The match is exact | A platform slug is set only when the issuer is identified. The BSC fallback above is the single exception |
| A new platform is not missed | An issuer that is recognisable but not yet named reports the generic launchpad slug rather than nothing |
| An ordinary DEX token is never labelled | Evidence that a liquidity pool token could also produce is excluded, so a Uniswap LP token never reports as launchpad issued |
| One platform can match several signals | Flap on Robinhood Chain is identifiable from its issuer and also runs a curve whose reserves are reported as a pool |
Related Fields
is_launchpad_token: tri-state.true: the token was issued by a launchpad platform.false: pools were resolved for this token and none of them, nor any provenance signal, indicates a launchpad.null: unknown. No pools were resolved, so no determination could be made. Do not read this as "not a launchpad".
launchpad_name: The launchpad platform slug (e.g."four.meme","pons","doppler", or the generic"launchpad").nullifis_launchpad_tokenis nottrue.is_launchpad(per pool):trueif the specific pool is a launchpad bonding curve.falseotherwise. A launchpad token that graduated, or that never ran a curve, hasis_launchpad_token: truewhile no pool indexhasis_launchpad: true; the two fields answer different questions.
Supply & Burn Features
decimals:
Number of decimal places the token uses (e.g., 18 for most ERC20 tokens). Used to convert between raw and human-readable amounts.
supply:
Total supply of the token, normalized by decimals (raw_supply / 10^decimals). This is the human-readable value (e.g., 1000000000 for a token with 1 billion supply).
burned_supply:
Total supply held by the burn addresses, normalized by decimals. The value is verified on-chain via multicall. 0 if no tokens have been sent to burn addresses.
burned_supply_pct:
Percentage of the total supply held by the burn addresses, capped to 2 decimal places (e.g., 6.13). 0 if no tokens have been burned.
circulating_supply:
Circulating supply of the token, calculated as supply - burned_supply. Represents the tokens that are still in active circulation.
burn_addresses:
An array of balances of the burn addresses. Each entry is a holder object with the burn address and its token balance. Only addresses with a balance > 0 are included.
"burn_addresses": [
{
"owner_address": "0x000000000000000000000000000000000000dead",
"token_amount": 61256610.0,
"pct_held": 6.13
}
]
normalized_supply (deprecated):
Use supply instead. Returns the same value as supply. Kept for backwards compatibility.
Burn addresses
A burn address is an address that nobody controls. The API uses one set of five on every chain:
| Address | Note |
|---|---|
0x0000000000000000000000000000000000000000 | The zero address |
0x000000000000000000000000000000000000dEaD | The usual dead address |
0x0000000000000000000000000000000000000001 | The ecrecover precompile. It has no code and no key |
0xdead000000000000000042069420694206942069 | A vanity burn address |
0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead | A vanity burn address |
The match is exact and ignores letter case. An address that only starts like a burn address is not a burn address. This set gives burned_supply, burn_addresses, the true value of ownership_renounced for a burn owner, and the burn_address refusal.
Since 2026-10-03 the set has five addresses, not two. So burned_supply, burned_supply_pct and circulating_supply can be different from an earlier response, with no new transfer. For example, on SHIB (Ethereum, live on 2026-10-03), 0xdead000000000000000042069420694206942069 holds 41.04% of the supply, so burned_supply_pct is 41.05. Before that date it was 0. Also, an owner getter that gives 0x…0001 now makes ownership_renounced true (REN on Ethereum is an example).
Other Features
name:
Name of the token.
symbol:
Symbol of the token.
creator_address:
Address of the creator (EOA) of the token.
creator_balance:
Amount of tokens held by the creator (normalized by decimals).
creator_percent:
Percentage of the total supply held by the creator of the token, capped to 2 decimal places.
top_holders:
Top 10 token holders, returned as an array of holder objects. Balances are verified on-chain via multicall. This list can include a burn address.
[
{
"owner_address": "0x...",
"token_amount": 1234567.89,
"pct_held": 12.34
}
]
Each entry in top_holders has the following fields:
owner_address: Address of the token holder.token_amount: Amount of tokens held by that address (normalized by decimals).pct_held: Percentage of the total token supply held by that address.
is_in_dex:
The token is in at least one DEX.
cannot_buy:
Reads only the pools with a settled buy result (buy_success is true or false).
| Value | Meaning |
|---|---|
true | Every pool with a settled buy result failed the buy |
false | At least one pool allowed a buy |
null | No pool has a settled buy result, so no buy was simulated. Examples: the token has no pool, or each pool is a bonding curve or has less than $100 of liquidity |
null is not false. It does not tell you that the token can be bought.
owner_can_change_balance:
The token owner can modify any holder's balance directly. This field is part of the extended analysis set and may be absent from the response entirely: an absent field means "not assessed", which is not the same as false. Key off its presence before reading it. Today the API does not run the owner simulation, so the field is absent.
is_launchpad_token:
true if the token was issued by a launchpad platform. false if pools were resolved and none indicates a launchpad. null if no pools were resolved, meaning no determination could be made; see Launchpad Detection for the full platform list.
launchpad_name:
The launchpad platform slug (e.g. "four.meme", "clanker", "doppler", "pons"). Falls back to the generic "launchpad" when the token is provably launchpad-issued but the platform is not yet named. null if is_launchpad_token is not true. The full list of slugs is in Launchpad Detection; treat it as an open set and keep a default branch, since new platforms are added over time.
last_scanned_at:
ISO 8601 UTC timestamp of when this token was last analyzed (e.g., "2025-01-15T12:30:00Z").
Token Safety Summary
The summary endpoint returns a compact, pre-processed view of a token's safety data, designed for dashboards and quick-scan UIs where you need actionable signals without parsing the full feature set.
curl -X 'GET' \
'https://tokin-api.dedaub.com/token/{chain}/{token_address}/summary' \
-H 'accept: application/json' \
-H 'X-API-Key: YOUR-API-KEY'
Response Fields
| Field | Meaning |
|---|---|
verdict | The overall verdict, from the same rule as the Tok{In} scan page: danger, warning, safe or not_tested. null only when the request produced no analysis (a 422 or a 500 response) |
verdict_reasons | Every cause of verdict that holds, most severe first: sell_failed, buy_failed, danger_flags, no_liquidity, warning_flags, no_trade_simulated. Empty when verdict is safe or null. More keys can come later, so show a key that you do not know as neutral |
active_flag_count | The number of checks that fired (status danger, warning or info). A check with no answer is not counted |
evaluated_flag_count | The number of checks that gave an answer. Use it as the denominator. It is active_flag_count + passed_flag_count + notice_flag_count |
total_flag_count | The number of checks in the catalogue. Active, passed, notice and unknown checks always add up to this value |
passed_flag_count | Checks that ran and found nothing |
notice_flag_count | Checks whose answer is neither a pass nor a finding. Only is_open_source today |
unknown_flag_count | Checks that gave no answer. Never show them as passes |
danger_flag_count, warning_flag_count, info_flag_count | Active checks at each severity |
flags | Every check, resolved, with key, label, status, active and value. The order is danger, warning, info, notice, safe, unknown |
highlights | access_restrictions, transfer_restrictions and has_enough_liquidity as resolved flags |
access_restrictions | The value of has_blacklist_or_whitelist. true is a finding |
transfer_restrictions | true when has_trading_cap_functionality or has_position_cap_functionality is true. true is a finding |
funds_can_be_drained | The same value as funds_can_be_drained in the full response |
has_enough_liquidity | The deepest pool has more than $100 of liquidity. true is the good answer |
total_liquidity_usd | The USD liquidity of all pools. null, not 0, when the API found no pool |
can_buy / can_sell | true when a buy (or a sell) succeeded in at least one simulated pool. false when it failed in every simulated pool. null when no trade was simulated |
detected_pool_count | The number of pools |
dominant_pool | The pool with the highest liquidity, or null if no pools exist |
⚠ Colour each check from status, never from value. The checks do not have the same polarity. A null value is no answer, not a pass. status is one of danger, warning, info, notice, safe and unknown.
The fields of dominant_pool:
type: Pool type (e.g.UniV2,UniV3).address: Pool contract address.liquidity: Liquidity in USD.can_buy/can_sell: Whether buys/sells succeed in this pool.nullwhen no trade was simulated in this pool.buy_tax/sell_tax: Tax as a percentage (e.g.1.0= 1%).nullif unavailable. The Swagger UI text for these fields says 0–1 today. That text is wrong. Use the scale on this page.
Example
curl -X 'GET' \
'https://tokin-api.dedaub.com/token/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48/summary' \
-H 'accept: application/json' \
-H 'X-API-Key: YOUR-API-KEY'
Result (USDC, live on 2026-10-03; flags is trimmed to 3 of 23 entries):
{
"verdict": "warning",
"verdict_reasons": ["warning_flags"],
"active_flag_count": 8,
"evaluated_flag_count": 23,
"total_flag_count": 23,
"passed_flag_count": 15,
"notice_flag_count": 0,
"unknown_flag_count": 0,
"danger_flag_count": 0,
"warning_flag_count": 7,
"info_flag_count": 1,
"flags": [
{"key": "is_proxy", "label": "Proxy Contract", "status": "warning", "active": true, "value": true},
{"key": "has_owner_functionality", "label": "Owner Privileges Mechanism", "status": "info", "active": true, "value": true},
{"key": "pause_status_can_be_modified", "label": "Pause Modifiable", "status": "safe", "active": false, "value": false}
],
"highlights": [
{"key": "access_restrictions", "label": "Access Restrictions", "status": "warning", "active": true, "value": true},
{"key": "transfer_restrictions", "label": "Transfer Restrictions", "status": "safe", "active": false, "value": false},
{"key": "has_enough_liquidity", "label": "Has Enough Liquidity", "status": "safe", "active": false, "value": true}
],
"access_restrictions": true,
"transfer_restrictions": false,
"funds_can_be_drained": false,
"has_enough_liquidity": true,
"total_liquidity_usd": 401633007.6150325,
"can_buy": true,
"can_sell": true,
"detected_pool_count": 42289,
"dominant_pool": {
"type": "UniV3",
"address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
"liquidity": 98505369.9597153,
"can_buy": true,
"can_sell": true,
"buy_tax": 0.0,
"sell_tax": 0.0
}
}
Since April 2026, the summary has no major_flags list. Use highlights and flags.
Example
curl -X 'GET' \
'https://tokin-api.dedaub.com/token/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48?include_token_price=true' \
-H 'accept: application/json' \
-H 'X-API-Key: YOUR-API-KEY'
Result (USDC, live on 2026-10-03; dex and top_holders are trimmed to one entry):
{
"chain": "ethereum",
"contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"features": {
"mint_or_burn_function": true,
"can_selfdestruct": false,
"transfer_guarded_by_storage": true,
"transfer_from_guarded_by_storage": true,
"trading_cooldown": false,
"view_function_modifies_state": false,
"has_blacklist_or_whitelist": true,
"external_call": true,
"timebomb": false,
"has_pause_functionality": true,
"pause_status_can_be_modified": false,
"has_owner_functionality": true,
"has_tax_functionality": false,
"tax_can_be_modified": false,
"has_trading_cap_functionality": false,
"trading_cap_can_be_modified": false,
"has_position_cap_functionality": false,
"position_cap_can_be_modified": false,
"balance_update_function": ["0xa9059cbb", "0x23b872dd", "0x2fc81e09", "0x40c10f19", "0x42966c68", "0xe3ee160e", "0xef55bec6", "0xcf092995", "0x88b7ab63", "0xf9f92be4", "0x430239b4", "0x2fc81e09", "0x1a895266"],
"allowance_update_function": ["0x39509351", "0x9fd5a6cf", "0xa457c2d7", "0xd505accf", "0x095ea7b3", "0x23b872dd"],
"name": "USD Coin",
"symbol": "USDC",
"decimals": 6,
"supply": 48963128240.24519,
"normalized_supply": 48963128240.24519,
"is_open_source": true,
"has_open_source_implementation": true,
"is_liquid": true,
"is_proxy": true,
"owner_address": "0xFcb19e6a322b27c06842A71e8c725399f049AE3a",
"ownership_renounced": false,
"send_tax": 0,
"receive_tax": 0,
"transfer_success": true,
"creator_address": "0x95ba4cf87d6723ad9c0db21737d862be80e93911",
"creator_balance": 2505.454092,
"creator_percent": 0,
"is_in_dex": true,
"cannot_buy": false,
"allowance_bypass": false,
"privileged_spenders": [],
"is_launchpad_token": false,
"launchpad_name": null,
"has_trading_cap": true,
"has_position_cap": false,
"timebomb_status": null,
"timebomb_deadlines": [],
"last_scanned_at": "2026-10-03T14:43:58Z",
"dex": [
{
"errorMessage": null,
"errorCode": null,
"pair": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
"liquidity_type": "UniV3",
"liquidity": 98505369.9597153,
"buy_success": true,
"buy_tax": 0,
"sell_success": true,
"sell_tax": 0,
"pool_id": null,
"is_launchpad": false
}
],
"top_holders": [
{
"errorMessage": null,
"errorCode": null,
"owner_address": "0x37305b1cd40574e4c5ce33f8e8306be057fd7341",
"token_amount": 4227515543.911332,
"pct_held": 8.634079757258096
}
],
"burn_addresses": [
{
"errorMessage": null,
"errorCode": null,
"owner_address": "0x000000000000000000000000000000000000dead",
"token_amount": 56328.52738,
"pct_held": 0.00011504274625513169
},
{
"errorMessage": null,
"errorCode": null,
"owner_address": "0x0000000000000000000000000000000000000001",
"token_amount": 27657.636544,
"pct_held": 5.64866615717311e-05
},
{
"errorMessage": null,
"errorCode": null,
"owner_address": "0xdead000000000000000042069420694206942069",
"token_amount": 474.365227,
"pct_held": 9.68821323409839e-07
}
],
"burned_supply": 84460.529151,
"burned_supply_pct": 0,
"circulating_supply": 48963043779.71604,
"funds_can_be_drained": false
},
"error": "",
"error_code": null,
"token_usd_price": 0.9999655272004394,
"market_cap_usd": 48961355886.52195,
"price_source": "defillama",
"price_age_seconds": null
}
USDC is a proxy. The API calls the owner getter at the proxy address, so the call reaches the implementation code, and owner_address is the live owner. So ownership_renounced is false, and each owner-gated setter keeps its static result. timebomb_status is null, because the code has no timebomb finding. Three burn addresses hold USDC. Their total is less than 0.005% of the supply, so burned_supply_pct is 0 after the rounding to 2 decimals. error_code is null because the request succeeded. Without include_token_price=true, the four price fields are null.
Example: owner not determined
BAT on Ethereum (0x0d8775f648430679a709e98d2b0cb6250d2887ef), live on 2026-10-03:
"owner_address": null,
"ownership_renounced": null,
"has_owner_functionality": true
BAT has no owner getter, so owner_address is null. A wallet is in a storage slot that the code uses for its owner checks. So the API cannot say that the owner role is gone, and ownership_renounced is null. Show this as "not determined", never as "renounced". With null, each owner-gated setter keeps its static result.
API Response Reference
All endpoints use JSON unless otherwise noted. Authentication endpoints redirect via 307.
Common Response Codes
| Code | Meaning |
|---|---|
200 | Success |
307 | Redirect (OAuth flows) |
400 | Bad request (invalid input, failed precondition) |
401 | Missing, invalid or expired API key / unauthenticated |
402 | A free trial allowance is spent. Not retryable; the allowance does not refill |
403 | Forbidden (e.g. refresh without an enterprise plan, or a new trial key after the trial ended) |
404 | Resource not found |
409 | Conflict (e.g. max API keys reached, or a trial key was already issued) |
422 | Validation error (invalid address format, missing params) |
429 | Rate limit or quota exceeded |
500 | Internal server error |
503 | Service not configured (billing) |
Token
GET /token/{chain}/{contract_address}
Returns token safety features.
Refusal codes
| Code | error_code | Meaning |
|---|---|---|
200 | null | TokenResponse with features |
422 | invalid_address | The address is not an EVM address |
422 | burn_address | One of the five burn addresses. It is not a token. Do not retry |
422 | not_a_contract | There is no code at the address on this chain |
422 | not_a_token | There is code, but it does not answer the ERC-20 calls (name, symbol, decimals, totalSupply) |
422 | invalid_token_data | The analysis gave a result that is not valid |
422 | (none) | Unknown chain or bad query value. FastAPI detail list |
500 | analysis_failed | A fault on our side. You can retry later |
401 | api_key_required | No key. See AGENTS.md §7.1 |
401 | api_key_expired | The key is past its expiry date |
401 | (none) | An unknown key: {"detail": "Invalid API Key"} |
402 | free_quota_exhausted | A trial allowance is spent. Do not retry |
403 | (none) | refresh or redecompile without an enterprise plan or an admin key |
429 | (none) | See Rate Limiting |
Branch on error_code, not on error. The error text can change. When error_code is absent, branch on the HTTP status. For a 422 or a 500 refusal with an error_code, the body is a TokenResponse with features: null. A 401 or a 402 with a code has the body {"error_code", "error", "detail", "next_step"}.
Burn address refusal (HTTP 422). The match ignores letter case:
{
"chain": "ethereum",
"contract_address": "0x000000000000000000000000000000000000dEaD",
"features": null,
"error": "Burn/null address — tokens sent here are permanently removed",
"error_code": "burn_address",
"token_usd_price": null,
"market_cap_usd": null,
"price_source": null,
"price_age_seconds": null
}
TokenResponse schema:
{
"chain": "ethereum",
"contract_address": "0x...",
"features": { ... },
"error": "",
"error_code": null,
"token_usd_price": null,
"market_cap_usd": null,
"price_source": null,
"price_age_seconds": null
}
Query param response_format=goplus returns GoPlus-compatible features shape.
Price fields
The four price fields are null unless the request has include_token_price=true. The API resolves a price only on request, because the price costs an extra call.
-
token_usd_price: The USD price of the token. -
market_cap_usd:token_usd_pricemultiplied by our owncirculating_supply, not the market cap of the source.nullwhen the circulating supply is not above zero, or when the result is not plausible. -
price_source: The source that gave the price. The API tries the sources in this order:Value Meaning defillamaA live aggregate across venues. The preferred source network_token_infoThe last trade price in our warehouse. It can be old, so read price_age_secondspool_spotThe spot price of the deepest pool of the token that simulated a real buy stablecoin_pin$1, for the stable anchors of the chain -
price_age_seconds: The age of the price in seconds. Set only fornetwork_token_info. The API reads the other sources live.
All four fields are null when no source has a price. A missing price is unknown, not zero.
GET /token/{chain}/{contract_address}/summary
Returns a compact safety summary. Uses the same cache as the full token endpoint.
| Code | Body / Detail |
|---|---|
200 | TokenSummaryResponse |
422 | TokenSummaryResponse with verdict: null, evaluated_flag_count: 0 and every check counted as unknown (unknown_flag_count equals total_flag_count). The address is not valid, is a burn address, has no code, or is not a token. The body has no error_code; call the token route for the reason. An unknown chain gives the FastAPI detail list instead |
500 | The same body as for 422 (analysis failure) |
401 | api_key_required or api_key_expired (with error_code), or {"detail": "Invalid API Key"} |
402 | free_quota_exhausted |
429 | See Rate Limiting |
TokenSummaryResponse schema: see Token Safety Summary for each field.
{
"verdict": "warning",
"verdict_reasons": ["warning_flags"],
"active_flag_count": 8,
"evaluated_flag_count": 23,
"total_flag_count": 23,
"passed_flag_count": 15,
"notice_flag_count": 0,
"unknown_flag_count": 0,
"danger_flag_count": 0,
"warning_flag_count": 7,
"info_flag_count": 1,
"flags": [{"key": "...", "label": "...", "status": "warning", "active": true, "value": true}],
"highlights": [{"key": "access_restrictions", "label": "Access Restrictions", "status": "warning", "active": true, "value": true}],
"access_restrictions": true,
"transfer_restrictions": false,
"funds_can_be_drained": false,
"has_enough_liquidity": true,
"total_liquidity_usd": 401633007.6150325,
"can_buy": true,
"can_sell": true,
"detected_pool_count": 42289,
"dominant_pool": {
"type": "UniV3",
"address": "0x...",
"liquidity": 98505369.9597153,
"can_buy": true,
"can_sell": true,
"buy_tax": 0.0,
"sell_tax": 0.0
}
}
Auth
POST /agent-keys
A free, capped API key for an automated caller that cannot sign in. Sign-in needs a mailbox, so software has no way through the OAuth flow; this is the way in for an agent or a script.
Requires no credential. Send a reason saying what you are building.
curl -X POST 'https://tokin-api.dedaub.com/agent-keys' \
-H 'Content-Type: application/json' \
| Field | Required | Notes |
|---|---|---|
reason | yes | Up to 500 characters. Recorded, and not verified. |
contact | no | Up to 200 characters. |
| Code | Body / Detail |
|---|---|
201 | {"api_key": "...", "plan": "agent", "api_calls_total": ..., "api_calls_per_day": ..., "rate_limit_per_minute": ..., "expires_at": "...", "next_step": "..."} |
403 | error_code: agent_trial_expired. The trial of this client network has ended. It does not renew |
409 | error_code: agent_trial_already_issued. This client network already has a trial key. The body has no key |
422 | Missing or over-long reason |
429 | error_code: agent_registration_throttled. Registration is closed for now. Wait for the Retry-After header, or sign in |
503 | Trial registration is unavailable on this deployment |
The trial allowance is small by design: enough to build and demonstrate an
integration against a handful of tokens, not enough to run a product on.
api_calls_total is a lifetime total, and expires_at is the time when the
key stops working. On 2026-10-03 a trial key had 100 calls in total, 25 calls
for each day, 20 calls for each minute and 5 calls in 10 seconds, and it
expired after 14 days. The 201 response does not show the limit for 10
seconds. Read the other values from the 201 response.
Every response to a key with a deadline has the header X-API-Key-Expires-At.
In the last 7 days, the response also has X-API-Key-Expires-In and a
Warning header.
The API issues one trial key for each client network (your IP address, or
your IPv6 block), and it shows the key one time. A second request from the
same client network gets 409 with
error_code: agent_trial_already_issued and no key. So keep the key that you
get. After the trial ends, a request gets 403 with
error_code: agent_trial_expired. When the allowance is spent, the API answers
402 with error_code: free_quota_exhausted. The allowance does not refill, so
a retry does not help. Sign in and create a full key instead.
AI coding agents: read AGENTS.md §2.2 and §7.1 before writing
any client against this.
GET /auth/login/{provider}
| Code | Detail |
|---|---|
307 | Redirect to OAuth provider (Google or GitHub) |
GET /auth/callback/{provider}
| Code | Detail |
|---|---|
307 | Redirect to {FRONTEND_URL}/auth/success?token=<jwt> on success |
400 | OAuth callback failure (provider error) |
Profile
All profile endpoints require a valid JWT (set via cookie from auth callback).
GET /profile/me
| Code | Body |
|---|---|
200 | User object |
401 | {"detail": "Not authenticated"} |
GET /profile/api-keys
| Code | Body |
|---|---|
200 | Array of APIKey objects |
401 | {"detail": "Not authenticated"} |
POST /profile/api-keys
| Code | Body / Detail |
|---|---|
200 | Created APIKey object |
401 | {"detail": "Not authenticated"} |
409 | {"detail": "Maximum of N API key(s) allowed per user"} |
500 | {"detail": "Error creating API key"} |
DELETE /profile/api-keys/{api_key_id}
| Code | Body |
|---|---|
200 | {"message": "API key deleted successfully"} |
200 | {"message": "API key not found or does not belong to the user"} (no-op) |
401 | {"detail": "Not authenticated"} |
GET /profile/logs
| Code | Body |
|---|---|
200 | Array of APILog objects |
401 | {"detail": "Not authenticated"} |
GET /profile/logs_status_codes
| Code | Body |
|---|---|
200 | Array of {"status_code": int, "count": int} |
401 | {"detail": "Not authenticated"} |
GET /profile/logs_chains
| Code | Body |
|---|---|
200 | Array of {"chain": string, "count": int} |
401 | {"detail": "Not authenticated"} |
GET /profile/logs_addresses
| Code | Body |
|---|---|
200 | Array of {"address": string, "count": int} |
401 | {"detail": "Not authenticated"} |
GET /profile/linked-accounts
| Code | Body |
|---|---|
200 | Array of {"id", "provider", "provider_id", "email", "linked_at"} |
401 | {"detail": "Not authenticated"} |
DELETE /profile/linked-accounts/{account_id}
| Code | Body / Detail |
|---|---|
200 | {"message": "Account unlinked successfully"} |
200 | {"message": "Account not found or does not belong to user"} (no-op) |
400 | {"detail": "Cannot unlink last provider account"} |
401 | {"detail": "Not authenticated"} |
Rate Limiting
429 responses are returned by the auth middleware for API-key-authenticated endpoints. Four independent checks:
| Check | Scope | Headers |
|---|---|---|
| Burst (10 s) | Per API key (Redis) | Retry-After, X-RateLimit-Burst-Limit, X-RateLimit-Burst-Window, X-RateLimit-Remaining: 0 |
| Per-minute QPS | Per API key (Redis) | Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining: 0 |
| Daily quota | Per user (Redis) | X-Quota-Daily-Limit, X-Quota-Daily-Used, Retry-After |
| Monthly quota | Per user (PostgreSQL) | X-Quota-Monthly-Limit, X-Quota-Monthly-Used |
A trial key also has a lifetime total (api_calls_total). The API checks it before the daily quota. When the total is spent, the API answers 402 with error_code: free_quota_exhausted, not 429. Do not retry a 402.
Admin users bypass all limits. A Host or Referer header does not bypass authentication or rate limiting.