---
title: "Scan a token"
method: POST
path: "/v1/token/scan"
tags: ["Scan v1"]
---

# Scan a token

`POST /v1/token/scan`

## Request

- Base URL: `https://api.blockaid.io`
- URL: `https://api.blockaid.io/v1/token/scan`
- Auth: one of:
  - API key in header `X-API-Key`
  - API key in header `X-CLIENT-ID`
- TypeScript: <https://skmtc.dev/blockaid-official/apis/blockaid-api/docs/v1/token/scan/post.ts> (a function with typed inputs; its response is checked against the schema)

Replaces `/v0/token/scan`. `chain` moves into the body.

## Request body

- V1TokenScanRequest
  - `asset` string, required: Token identifier. A bare contract address on most chains, but carries composite identity where the chain requires it — e.g. `CODE-ISSUER` on Stellar, a typed string on Sui, `0.0.x` on Hedera. `chain` stays a separate field; this string does not encode it.
  - `chain` string, required: Chain identifier. Open string rather than a closed enum, because supported chains differ per surface and expand without a major version. See the per-surface support matrix in the docs.
  - `metadata` V1ScanRequestMetadata: Request context, carried forward from v0 `ScanRequestMetadata`. The v0 `domain` and `non_dapp` fields are dropped in v1 — both are superseded by `connection.origin`.
    - `account` V1Account
      - `account_id` string, required: Unique identifier for the account.
      - `account_creation_timestamp` string, date-time: Timestamp when the account was created.
      - `user_age` integer: Age of the user in years.
      - `user_country_code` string: ISO country code of the user's location.
      - `account_addresses` string[]: List of all account addresses in different chains based on the CAIPs standard (https://github.com/ChainAgnostic/CAIPs). Ethereum mainnet example: eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb
    - `connection` V1Connection
      - `user_agent` string: User agent string from the client's browser or application.
      - `ip_address` string, ipvanyaddress, required: IP address of the customer making the request. Both IPv4 and IPv6 addresses are supported.
      - `origin` string, uri: The full URL of the website that initiated the request — the same direction as the HTTP `Origin` header.
      - `walletconnect_name` string: WalletConnect session name, when the request originates from a WalletConnect session.
      - `walletconnect_description` string: WalletConnect session description, when the request originates from a WalletConnect session.

## Response `200`

Scan completed

- V1TokenScanResponse
  - `request_id` string, required
  - `metadata` V1TokenMetadata, required
    - `entity_type` 'TOKEN' | 'ADDRESS' | 'TRANSACTION' | 'SIGNATURE' | 'WEBSITE', required: Closed enum. Clients must branch exhaustively. `SIGNATURE` is reserved and has no endpoint in v1.
    - `details` V1TokenMetadataDetails, required
      - `address` string, required
      - `chain` string, required: Chain identifier. Open string rather than a closed enum, because supported chains differ per surface and expand without a major version. See the per-surface support matrix in the docs.
  - `risk` V1Risk, required: Three states, and only three: - **Unknown** — both arrays empty. Nothing was found under the active profile and nothing vouches for the entity. `severity` is `{ low, 10 }`. This is the common case, not an error. - **Risk found** — `negative_categories` populated, `positive_categories` empty. - **Trusted** — `positive_categories` populated, `negative_categories` empty. `severity` is `{ none, 0 }`. Both arrays populated at once does not occur. An empty `negative_categories` never means the entity is safe.
    - `severity` V1Severity, required: The verdict. `level` is bucketed from `score`; the two always agree, so a client may branch on either.
      - `level` 'none' | 'low' | 'medium' | 'high' | 'critical', required: Risk severity level. Closed enum, bucketed from `risk.score`. Replaces the three inconsistent v0 `ValidationResultType` enums. `none` = 0, requires a populated `positive_categories`. `low` = 10–30, and covers two states: unknown (score exactly 10, `negative_categories` empty) and a real but minor finding (`negative_categories` populated). `medium` = 31–60. `high` = 61–89. `critical` = 90–100. Nothing lands between 1 and 9. v0's `Error` result type has no equivalent — failures are now the error envelope.
      - `score` integer, required: Note the type change from v0, where `malicious_score` was a string. 0 when `positive_categories` is populated; otherwise `max(10, max(category scores))`. The unknown baseline of 10 is a floor findings push up from, so a weak detection never scores below an entity nothing is known about. Not verifiable by customers: its inputs are internal-only.
    - `profile` V1Profile, required: Resolved from the credential on the request (`X-API-Key` or `X-CLIENT-ID`), which resolve a profile identically. An object rather than a bare string so fields can land without a breaking change. Custom profile ids are not in v1 — every id returned is one of the managed set.
      - `id` string, required
    - `negative_categories` V1NegativeCategory[], required: Security, fraud and compliance findings in one array, so `negative_categories.length === 0` is a safe check. Splitting compliance out would let a sanctions hit slip past the most common integrator branch.
      - `category` string, required: Flat value from the register — replaces v0's `category` + `sub_category` pair and the `interaction_with_*` compound names, whose directness now lives in `proximity`. Open string. To route findings to an owning team, map `category` client-side; the register carries that mapping and it does not vary per scan.
      - `proximity` 'ownership' | 'counterparty' | 'indirect': Closed enum. How far the scanned entity stands from the risk. Same vocabulary as v0 `Proximity`, minus `overall` — that value is a request-side filter on `/v0/address/risk-exposure`, not a response value. `ownership` — it is the thing, or operates it. `counterparty` — direct interaction. `indirect` — traced value, one or more hops removed. `indirect` collapses every depth beyond one hop. Where hop count matters it rides in `signals[].evidence` as a display value and is not branchable. On a transaction this describes the **cited participant**, not the transaction itself. Returned on the `risk_exposure` profile only — see `risk.profile`. Under any other profile the field is absent.
      - `name` string, required: Display name for the category.
      - `description` string, required: Short display text, composed server-side and keyed on (`category`, `proximity`). **Print verbatim.** This is the only field carrying the attacker/victim distinction: at `proximity: "ownership"` a drainer and the wallet it emptied return the same category at the same level.
      - `signals` V1Signal[], required: The signals that produced this category. May be empty.
        - `id` string, required: Stable namespaced key. Open string.
        - `name` string, required: End-user-facing title. Static per signal id.
        - `description` string, required: End-user-facing explanation. Static per signal id; the per-scan specifics are in `evidence`.
        - `evidence` V1Evidence, required: The per-scan specifics this signal cited, as `{ key: value }`. Always present, may be empty. Keys are drawn from a registered vocabulary with a declared type, so the platform renders by type rather than per signal id. A key means the same thing and carries the same type wherever it appears. Keys are additive and never break existing clients. Registered types: `address`, `address[]`, `tx_hash`, `timestamp`, `percentage`, `number`, `label`, `boolean`, `url`. Evidence is not a scoring input and must not be branched on — keys are open strings.
    - `positive_categories` V1PositiveCategory[], required
      - `category` string, required: Flat value from the register. Open string, same vocabulary as `V1NegativeCategory.category`.
      - `name` string, required: Display name for the category.
      - `description` string, required: Short display text for the category.
      - `signals` V1Signal[], required
        - `id` string, required: Stable namespaced key. Open string.
        - `name` string, required: End-user-facing title. Static per signal id.
        - `description` string, required: End-user-facing explanation. Static per signal id; the per-scan specifics are in `evidence`.
        - `evidence` V1Evidence, required: The per-scan specifics this signal cited, as `{ key: value }`. Always present, may be empty. Keys are drawn from a registered vocabulary with a declared type, so the platform renders by type rather than per signal id. A key means the same thing and carries the same type wherever it appears. Keys are additive and never break existing clients. Registered types: `address`, `address[]`, `tx_hash`, `timestamp`, `percentage`, `number`, `label`, `boolean`, `url`. Evidence is not a scoring input and must not be branched on — keys are open strings.
  - `properties` V1TokenProperties: Neutral facts about the scanned token or NFT collection, separate from `risk`. Grouped into namespaces: `metadata` (what it is), `creation` (how it came to exist), `authorities` (who controls it), `fees` and `limits` (what its own code charges and caps), `holdings` (who holds it), `market` (price and volume) and `liquidity` (DEX pools). Rules for every field: `null` means no value was returned. Usually that means we checked and there is nothing there; a field we don't read on the token's chain also comes back `null` (the coverage table in the docs lists these). A field is left out only when it does not apply to the token or its chain. Lists are always present; `[]` means nothing was returned. When we can't identify the asset, the response has no `properties`. Every number is for the scanned chain only. Token quantities use the `V1Amount` shape.
    - `metadata` V1TokenPropertiesMetadata: What the token is: its type, name, symbol, decimals, logo, description and links.
      - `entity_type` string, required: What the asset is: `token` (fungible) or `nft` (a collection). Open list: accept values you don't know, more kinds may be added.
      - `name` string, nullable, required: Name of the token or collection.
      - `symbol` string, nullable, required: Ticker symbol.
      - `decimals` integer: Number of decimal places of the smallest unit. Converts every `V1Amount` (`value` = `raw_value` ÷ 10^decimals). Always set for fungible tokens; left out for NFTs.
      - `logo_url` string, nullable, required: URL of the token's logo image.
      - `description` string, nullable, required: Description the token publishes about itself.
      - `external_links` V1ExternalLinks, required: Official links published for the token, as full URLs.
        - `homepage` string, nullable: Official website.
        - `twitter` string, nullable: X (Twitter) profile, as a full URL such as `https://x.com/<handle>`.
        - `telegram` string, nullable: Telegram channel or invite, as a full URL such as `https://t.me/<name>`.
      - `urls` string[], required: Other URLs found in the token's metadata. `[]` = none.
    - `creation` V1TokenCreation: How the token came to exist: when it was created and by which accounts.
      - `timestamp` string, date-time, nullable: When the token was created, in UTC (RFC 3339, ending in `Z`).
      - `deployer` string, nullable: The account or contract that executed the creation. When a factory or launchpad created the token, this is the factory. Native address format of the chain.
      - `creator` string, nullable: The account that started the creation. Same as `deployer` when no factory was involved.
    - `authorities` V1TokenAuthority[]: Accounts that hold a control role over the token, such as the contract owner or the Solana mint authority. Only roles that exist on the token's chain and that we read there are listed (the coverage table in the docs lists them), so `[]` on a chain where we don't read roles means no roles were returned, not that nobody holds one. An entry with `address: null` means we checked and nobody holds the role (for example, the owner renounced it).
      - `role` string, required: The role held. Open list: `owner`, `mint_authority`, `freeze_authority`, `update_authority`, `permanent_delegate`; new roles may be added.
      - `address` string, nullable, required: The account that holds the role, in the chain's native address format. `null` means we checked and nobody holds the role.
    - `fees` V1TokenFees: Fees (often called taxes) built into the token's own code, per operation. These are not the DEX pool's swap fee and not network gas. Tokens with a tax often charge only when a DEX pool is on one side, so `buy`, `sell` and `transfer` can differ. An operation is `null` when no value was returned (not measured, or the simulation failed). `{ "percentage": 0 }` means measured, no fee.
      - `buy` V1TokenFee: A fee on one operation.
        - `percentage` number, required: Fee as a percentage, 0–100 (5 = 5%). `0` = measured, no fee.
        - `max_amount` V1Amount: A quantity of the scanned token. `value` is the human-readable amount; `raw_value` is the same amount in the token's smallest unit. Both are decimal strings, to keep full precision. `value` = `raw_value` ÷ 10^`metadata.decimals`.
          - `value` string, required: Amount after applying `decimals`, as a decimal string.
          - `raw_value` string, required: Amount in the token's smallest unit, as a decimal string (no `0x`).
      - `sell` V1TokenFee: A fee on one operation.
        - `percentage` number, required: Fee as a percentage, 0–100 (5 = 5%). `0` = measured, no fee.
        - `max_amount` V1Amount: A quantity of the scanned token. `value` is the human-readable amount; `raw_value` is the same amount in the token's smallest unit. Both are decimal strings, to keep full precision. `value` = `raw_value` ÷ 10^`metadata.decimals`.
          - `value` string, required: Amount after applying `decimals`, as a decimal string.
          - `raw_value` string, required: Amount in the token's smallest unit, as a decimal string (no `0x`).
      - `transfer` V1TokenFee: A fee on one operation.
        - `percentage` number, required: Fee as a percentage, 0–100 (5 = 5%). `0` = measured, no fee.
        - `max_amount` V1Amount: A quantity of the scanned token. `value` is the human-readable amount; `raw_value` is the same amount in the token's smallest unit. Both are decimal strings, to keep full precision. `value` = `raw_value` ÷ 10^`metadata.decimals`.
          - `value` string, required: Amount after applying `decimals`, as a decimal string.
          - `raw_value` string, required: Amount in the token's smallest unit, as a decimal string (no `0x`).
    - `limits` V1TokenLimits: Caps set by the token's own code on how much one account can buy, sell or hold. Fungible tokens only. Each value is `null` when no cap is set. Caps can change while the token runs.
      - `max_buy` V1Amount: A quantity of the scanned token. `value` is the human-readable amount; `raw_value` is the same amount in the token's smallest unit. Both are decimal strings, to keep full precision. `value` = `raw_value` ÷ 10^`metadata.decimals`.
        - `value` string, required: Amount after applying `decimals`, as a decimal string.
        - `raw_value` string, required: Amount in the token's smallest unit, as a decimal string (no `0x`).
      - `max_sell` V1Amount: A quantity of the scanned token. `value` is the human-readable amount; `raw_value` is the same amount in the token's smallest unit. Both are decimal strings, to keep full precision. `value` = `raw_value` ÷ 10^`metadata.decimals`.
        - `value` string, required: Amount after applying `decimals`, as a decimal string.
        - `raw_value` string, required: Amount in the token's smallest unit, as a decimal string (no `0x`).
      - `max_holding` V1Amount: A quantity of the scanned token. `value` is the human-readable amount; `raw_value` is the same amount in the token's smallest unit. Both are decimal strings, to keep full precision. `value` = `raw_value` ÷ 10^`metadata.decimals`.
        - `value` string, required: Amount after applying `decimals`, as a decimal string.
        - `raw_value` string, required: Amount in the token's smallest unit, as a decimal string (no `0x`).
    - `holdings` V1TokenHoldings: Who holds the token and how much: total supply, holder count, the largest holders, the share held by known wallet groups, and the balances of the token's own contract, owner and deployer. Supply, holder count and top holders cover tokens and NFTs; the rest is fungible-only.
      - `total_supply` V1Amount, required: A quantity of the scanned token. `value` is the human-readable amount; `raw_value` is the same amount in the token's smallest unit. Both are decimal strings, to keep full precision. `value` = `raw_value` ÷ 10^`metadata.decimals`.
        - `value` string, required: Amount after applying `decimals`, as a decimal string.
        - `raw_value` string, required: Amount in the token's smallest unit, as a decimal string (no `0x`).
      - `holders_count` integer, nullable, required: Number of distinct holders on the scanned chain.
      - `top_holders` V1TokenTopHolder[], required: The largest holders, largest first. `[]` = none found.
        - `address` string, required: Holder address. On Solana, the wallet that owns the token account.
        - `holding_percentage` number, nullable, required: Share of total supply held, 0–100.
        - `alias` string, nullable, required: Known name of the holder, such as an exchange or a DEX pool. `null` = no known name.
      - `dev_holding_percentage` number, nullable: Share of supply held in known developer wallets, 0–100.
      - `insiders_holding_percentage` number, nullable: Share of supply currently held by insiders: wallets whose early buying looks like insider activity, 0–100.
      - `snipers_holding_percentage` number, nullable: Share of supply currently held by sniper bots, 0–100.
      - `initial_snipers_holding_percentage` number, nullable: Share of supply sniper bots held at launch, 0–100. Compare with `snipers_holding_percentage` to see how much they still hold.
      - `bundlers_holding_percentage` number, nullable: Share of supply currently held by bundlers: wallets that bought in the exact same slot, at any point in the token's life, 0–100.
      - `balances` V1TokenBalances: How much of this token is held by the token's own contract, its owner and its deployer.
        - `contract` V1Amount: A quantity of the scanned token. `value` is the human-readable amount; `raw_value` is the same amount in the token's smallest unit. Both are decimal strings, to keep full precision. `value` = `raw_value` ÷ 10^`metadata.decimals`.
          - `value` string, required: Amount after applying `decimals`, as a decimal string.
          - `raw_value` string, required: Amount in the token's smallest unit, as a decimal string (no `0x`).
        - `owner` V1Amount: A quantity of the scanned token. `value` is the human-readable amount; `raw_value` is the same amount in the token's smallest unit. Both are decimal strings, to keep full precision. `value` = `raw_value` ÷ 10^`metadata.decimals`.
          - `value` string, required: Amount after applying `decimals`, as a decimal string.
          - `raw_value` string, required: Amount in the token's smallest unit, as a decimal string (no `0x`).
        - `deployer` V1Amount: A quantity of the scanned token. `value` is the human-readable amount; `raw_value` is the same amount in the token's smallest unit. Both are decimal strings, to keep full precision. `value` = `raw_value` ÷ 10^`metadata.decimals`.
          - `value` string, required: Amount after applying `decimals`, as a decimal string.
          - `raw_value` string, required: Amount in the token's smallest unit, as a decimal string (no `0x`).
    - `market` V1TokenMarket: Price and trading volume in USD, for the scanned chain.
      - `price_usd` number, nullable: Price of one whole token in USD. For an NFT collection, the last sale price of any item, at today's USD (not a floor price).
      - `volume_24h_usd` number, nullable: Trading volume over the last 24 hours, in USD.
    - `liquidity` V1TokenLiquidity: The token's DEX liquidity: total reserve, how much of it is locked or burned, and the pools. Fungible tokens only.
      - `total_reserve_usd` number, nullable, required: Total USD value of the token's DEX pools.
      - `locked_liquidity_percentage` number, nullable, required: Share of pool liquidity that is locked, weighted by pool reserve, 0–100. `null` on Solana, where locking isn't measured separately; LP burns are in `burned_liquidity_percentage`.
      - `burned_liquidity_percentage` number, nullable, required: Share of pool liquidity that is burned, weighted by pool reserve, 0–100.
      - `pools` V1TokenPool[], required: The pools that trade the token. `[]` = no pools.
        - `market_type` 'bonding_curve' | 'amm' | 'unknown' | 'null', nullable, required: Kind of market: `amm`, `bonding_curve` or `unknown`.
        - `market_name` string, nullable, required: The DEX or launchpad.
        - `pair_name` string, nullable, required: Pair name as the DEX writes it. The order may differ from `base_token` / `quote_token`.
        - `address` string, nullable, required: Pool address, or the pool id where the DEX has no pool contract (for example Uniswap v4).
        - `base_token` string, required: The scanned token, in its native address format.
        - `quote_token` string, required: The other token in the pool.
        - `reserve_usd` number, nullable, required: USD value of the pool's reserves.

## Other responses

- `400`: `MALFORMED_REQUEST` — body is not parseable: truncated payload, wrong Content-Type, invalid JSON. Not retryable.
- `401`: `UNAUTHENTICATED` — missing, malformed, or revoked credential. Not retryable.
- `403`: `FORBIDDEN` — key is valid but not entitled to the requested surface, chain, or profile. Not retryable.
- `422`: `VALIDATION_FAILED` — parses cleanly, violates the schema. Carries `error.details[]`. Not retryable.
- `429`: `RATE_LIMITED` — over quota. Retryable; honour `Retry-After`.
- `500`: `INTERNAL_ERROR` — unhandled failure inside the scan path. Retryable with backoff.
- `502`: `UPSTREAM_FAILED` — a dependency required to complete the scan returned an error. Retryable with backoff.
- `504`: `UPSTREAM_TIMEOUT` — the scan did not complete within the time limit. Retryable with backoff.

## Changes

> 120 revisions in range; 1 not diffed.

- **2026-10-29** `6a9a1f3974d3`: 3 breaking, 1 warning, 1 info
  - added the new required request property `asset`
  - removed the required property `metadata/id` from the response with the `200` status
  - removed the required property `policy` from the response with the `200` status
  - removed the request property `address`
  - …1 more
- **2026-10-29** `53277726e822`: 3 warning, 1 info
  - removed the optional property `metadata/first_scanned` from the response with the `200` status
  - removed the optional property `metadata/last_updated` from the response with the `200` status
  - added the new `approve` enum value to the `policy/action/anyOf[subschema #1]/` response property for the response status `200`
  - removed the `allow` enum value from the `policy/action/anyOf[subschema #1]/` response property for the response status `200`
- **2026-10-08** `cec32ae8b816`: 1 info
  - endpoint added
- **2026-10-08** `5f40f1064bf0`: 1 breaking
  - api path removed without deprecation
- …earlier changes not shown

[Operation history](https://skmtc.dev/blockaid-official/apis/blockaid-api/changes/v1/token/scan/post.md)

---

[API](https://skmtc.dev/blockaid-official/apis/blockaid-api.md) - [All operations](https://skmtc.dev/blockaid-official/apis/blockaid-api/llms.txt) - [OpenAPI document](https://skmtc.dev/blockaid-official/apis/blockaid-api/revisions/104550c9d3f1?raw)
