---
title: "Push entity labels (operator keys only)"
method: POST
path: "/v1/labels"
tags: ["Write"]
---

# Push entity labels (operator keys only)

`POST /v1/labels`

## Request

- Base URL: `https://intelapi.publicaml.org`
- URL: `https://intelapi.publicaml.org/v1/labels`
- Auth: API key in header `X-Signature` and API key in header `X-API-Key` and API key in header `X-Timestamp`

OPERATOR KEYS ONLY since 19.10.2026: a signed request from a key whose user_id is in ADMIN_USER_IDS. Any other credential gets 403 `admin_required` (or `hmac_required` for a bare key). Every entry names the `source` it is written under — an attribution_trust source, see GET /v1/admin/source-trust — and the row records the writer: `written_by` (user_id) and `written_by_key` (key fingerprint). Source and writer are different facts.

An address that nothing else describes is answerable by /v2/enrich IMMEDIATELY; one that a feed already describes waits for the next label-sync cycle (about ten minutes), which decides by source rank who wins. The response says which is which: `visible_now` and `visible_after_sync`. Rows from other sources on the same address are reported per entry in `competing` (with their rank), not refused: rank settles it. Re-pushing under the same source is an ordinary update; an unknown source is refused per entry (`unknown source`) — create it first.

An entry may name a CLUSTER instead of an address: send `cluster_id` (the value /v2/enrich returns in `axes.membership.group_id`) in place of `wallet_address` and ONE row covers the whole cluster, including members discovered later. Use it for hubs and for multi-address entities — a hub can hold 1.96M addresses, and a claim expanded over them freezes on today's membership. A cluster label participates in attribution with its original source and source rank, just like an address label. Confidence breaks rank ties; an address claim wins an otherwise equal tie. Inherited axis claims carry `applies_to: cluster` and `group_id`. No label-sync wait is needed for cluster claims. Sending both keys in one entry is a 400.

ONLY OWNERSHIP CLUSTERS MAY BE LABELLED: `unnamed:` (a hub) and `entity:`. `community:` and `cex_origin:` are refused — they group addresses by graph behaviour, not by owner, and a name put on one names strangers (the largest community cluster holds 523k addresses, the largest cex_origin 29.6M). Clusters above `CLUSTER_LABEL_MAX_MEMBERS` members are refused too; check `cluster_size` from /v1/entities first.

`category` must exist in the published taxonomy — see GET /v1/categories. An unknown one is refused per entry, with the nearest known names in `did_you_mean`.

SOLANA (label-only chain) is accepted since 19.10.2026. A Solana row is read by /v2/enrich straight from the label store, so `visible_now` counts it when your row is the best-ranked claim on the address; a row outranked by a feed is stored but not shown, and no sync cycle changes that. Addresses are case-sensitive: push the exact spelling. `cluster_id` entries do not apply to Solana (no clusters there).

## Request body

- object
  - `labels` object[], required
    - `source` string, required: attribution_trust source the row is written under (its rank decides disputes). Must exist: GET /v1/admin/source-trust.
    - `wallet_address` string: The address to look up, in its normal display form. * bitcoin — base58 (`1…`, `3…`) or bech32/bech32m (`bc1…`, `bc1p…`). Case is SIGNIFICANT for base58; do not lowercase it. A 64-character lowercase TRANSACTION HASH is also accepted here: on bitcoin the graph is bipartite (address → transaction → address), so a transaction is a node with its own neighbours and its own findings. CoinJoin is classified on the transaction, not on the participants, and this is how you read it. * ethereum, bsc — `0x` + 40 hex characters, any case. * tron — base58 (`T…`, 34 characters); the `0x` 20-byte form is accepted too and is what the API stores and echoes.
    - `chain` 'bitcoin' | 'ethereum' | 'bsc' | 'tron' | 'solana': Which blockchain the address belongs to. Send one of the values in the enum. The short forms used by the older clients (ETH, BTC, TRX, BNB) and any capitalisation are still accepted, but responses always echo the canonical spelling. Getting this wrong is the single commonest integration bug: the same 0x-prefixed address exists on ethereum, bsc and tron and means a different wallet on each, so the chain is part of the identity, never a hint. If it does not match the address format the call fails with `invalid_address_format`, which names the chain the address does look like. `solana` (short form `SOL`) is accepted here as well as by the enrich endpoints: a Solana label is stored and served by /v2/enrich from attribution alone (no graph, no score). Solana addresses are case-sensitive and are stored exactly as sent; a malformed one is refused per entry with `invalid_address_format`.
    - `cluster_id` string: Label the whole cluster instead of one address, e.g. 'unnamed:1:0x…' or 'entity:1:coinbase'. The chain is read out of the id; do not also send `chain`.
    - `category` string, required: Entity type from GET /v1/categories: cex, mixer, gambling, … Free-text values are refused.
    - `label` string: Display name.
    - `confidence` number

## Response `200`

Per-entry result.

- object
  - `upserted` integer: Address labels written.
  - `cluster_upserted` integer: Cluster labels written. Present only when the batch contained any — one of these covers a whole cluster, so it is not summed with `upserted`.
  - `visible_now` integer: Of `upserted`, how many are already answerable by /v2/enrich.
  - `visible_after_sync` integer: The rest: addresses something else already describes, where precedence is settled by the next label-sync cycle.
  - `errors` object[]: Per-entry rejections, each with its index. A cluster_id that matches no known cluster is rejected as entity_not_found; a community/cex_origin or oversized cluster is rejected with its `cluster_type` and `cluster_size`; an unknown category is rejected with `did_you_mean`.

## Other responses

- `400`: Malformed request.
- `403`: hmac_required — an API key alone cannot write.
- `429`: Rate limited: anonymous callers, and keys issued with a quota of their own. Authenticate to remove the limit.
- `500`: Server error.

---

[API](https://skmtc.dev/publicaml/apis/publicaml-intelligence-api.md) · [All operations](https://skmtc.dev/publicaml/apis/publicaml-intelligence-api/llms.txt) · [OpenAPI document](https://skmtc.dev/publicaml/apis/publicaml-intelligence-api/revisions/7c0c6315088c?raw)
