---
title: "Follow an address's money forward to where it was cashed out"
method: POST
path: "/v1/trace"
tags: ["Tracing"]
---

# Follow an address's money forward to where it was cashed out

`POST /v1/trace`

## Request

- Base URL: `https://intelapi.publicaml.org`
- URL: `https://intelapi.publicaml.org/v1/trace`
- Auth: one of:
  - API key in header `X-API-Key`
  - none

Value-weighted forward walk. Follows the largest outgoing flows hop by hop and reports two things: `cashout_summary` (funds that reached a labelled exchange / OTC / mixer / service — where the money left the chain's anonymous part) and `hanging` (funds sitting in unlabelled dead ends).

Bounded by wall clock, not by completeness: a partial walk is returned rather than a timeout, so `hops_traced` may be below `max_hops`. This is a lead-generating tool, not an exhaustive accounting of the funds.

On ethereum and bsc the walk continues THROUGH a DEX (`terminals[].transit`, with the swap in `terminals[].swaps[]`). A swap whose output returned to the sender, or that was settled in a batch with other people's orders, is listed with `followed: false` and a `reason`: in a batch auction net balances cannot say which recipient received this sender's leg, so no branch is attributed to it rather than attributing all of them. Such an entry carries `candidates` — the receivers that did not take the asset this sender was selling, offered as leads. When exactly one candidate is an address the same sender/venue pair paid in a settlement that WAS attributable, that branch is walked and marked `attribution: same-payee-as-attributable-settlement`.

A cross-chain bridge is a terminal: the walk is single-chain and stops there. Where one leg of the crossing is a protocol vault we hold a registry for, the terminal additionally carries `crosschain[]`: the SAME vault of that protocol on another chain, matched 1:1 on the source's own key, each entry with its own `chain` and `hop: 0`. Read it as an OBSERVATION, never as an edge: it carries no amount and no direction (a bridge pool is a shared mailbox, N legs in against M legs out), it moves no risk across the chain boundary, and the node on the far side is hop 0 of a SEPARATE walk you have to run on that chain. Absent unless the deployment enables it.

## Request body

- object
  - `address` string, required: The address whose outgoing funds to follow. 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', required: Which blockchain the address belongs to. Send one of the four 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.
  - `max_hops` integer: Default 5 (8 on bitcoin). On bitcoin one hop is HALF an economic step: the graph runs address → transaction → address.
  - `max_width` integer: How many outgoing edges to follow per address, largest first. A value outside 2-12 is pinned and named in `clamped`.
  - `max_nodes` integer: Ceiling on addresses visited. Hitting it stops the walk with `node_ceiling` among the cut reasons; the frontier is reported as still moving. A value outside the range is pinned and named in `clamped`.
  - `budget_sec` number: Wall-clock ceiling, clamped to 1-90 s. Default 15 s on ethereum/bsc (swap-through costs a query per DEX hop), 8 s on other EVM chains, 40 s on bitcoin. A value outside the range is pinned and named in `clamped`.

## Response `200`

The forward trace.

- object
  - `address` string
  - `chain` string
  - `hops_traced` integer: Below `max_hops` means the budget ran out, not that the money stopped.
  - `elapsed_ms` integer
  - `cashout_summary` object[]: Where the money left, biggest first.
    - `entity` string, nullable: Venue name, when known.
    - `category` string
    - `value_raw` number: Cross-token 18-decimal weight, comparable between rows but NOT a coin amount. Never show it to a person on its own.
    - `assets` object[]: The same value in the coins that actually arrived — this is the figure to display.
      - `symbol` string
      - `amount` number
    - `value_unknown` boolean: The graph does not carry a value for this branch: `value_raw` is 0 because it is unknown, not because the amount was small. Such a branch is never removed as dust.
    - `hits` integer
    - `first_hop` integer
    - `example_address` string
  - `terminals` object[]: Every terminal venue reached, one shape, deduped.
    - `kind` string
    - `address` string
    - `entity` string, nullable
    - `category` string, nullable
    - `hop` integer, nullable
    - `value_raw` number, nullable
    - `transit` boolean: The walk also came out the far side of this venue (a DEX).
    - `crosschain` object[]: The same protocol vault on ANOTHER chain. An observation matched on the source's key, not an observed transfer: no amount, no direction, no risk crosses, and `hop` restarts at 0 because the walk on that chain is a separate one. Present only on a terminal whose address is in the registry, and only when the deployment enables the step.
      - `via` string: Tag marking the entry as synthetic. Always prefixed `crosschain:`.
      - `kind` string
      - `method` string: What the two addresses were matched ON.
      - `chain` string: The chain of THIS node, not of the trace.
      - `address` string: Often the same string as the terminal: one protocol key yields one address across EVM chains. Same string, different chain, different account.
      - `hop` 0: Always 0: hop budget and decay of the traced chain do not extend across the boundary.
      - `entity` string, nullable
      - `category` string, nullable
      - `key` object: The identity the match rests on.
        - `type` string
        - `value` string
      - `vault_status` string, nullable: Protocol status of the vault when the registry last saw it (e.g. ActiveVault, InactiveVault, Retired). A retired vault still receives deposits and is still protocol infrastructure.
      - `source` string
      - `source_rank` integer, nullable: How much the registry's source is trusted, 0-100.
      - `source_confidence` number, nullable: The same trust as 0..1, added next to every named source in this API. `source_rank` 90 reads as 0.9.
      - `carries_value` false
      - `carries_risk` false
      - `observed` false: False always: nothing was observed moving between these two addresses.
      - `note` string
  - `hanging` object[]: Unlabelled dead ends — funds that are resting, not exited.
  - `still_moving` object[]: Addresses the walk stopped at because of the hop or time limit, not because the money stopped.
  - `partial` boolean: True when the walk was cut short, by the clock or by the node ceiling. The result is a lead, not a complete accounting.
  - `stop_reason` 'done' | 'deadline' | 'node_ceiling' | 'max_hops': Why the walk ended.
  - `effective` object: The limits the walk actually ran with, including `nodes_visited` and the `max_nodes` ceiling.
    - `max_hops` integer
    - `max_width` integer
    - `budget_sec` number
    - `max_nodes` integer
    - `nodes_visited` integer
  - `clamped` string[]: Request fields the server pinned to a supported range.

## Other responses

- `400`: Malformed request. A numeric parameter that is not a number answers `invalid_parameters`: `invalid[]` names every bad field with the reason and the accepted range, and `limits` lists the ranges of all numeric fields of this endpoint. A number OUTSIDE its range is not an error: it is pinned to the range and named in `clamped` (where the response has one).
- `429`: One of two things, told apart by `error`: * `rate limit exceeded` — your hourly quota (anonymous callers, and keys issued with a quota of their own). Wait `retry_after` seconds; authenticating removes the limit. * `server_busy` — this endpoint is a graph walk or a range scan, and the server is already running as many of those at once as it admits. Nothing was charged for the work. Retry the SAME request after `Retry-After` seconds (a few); do not split it into smaller calls, that multiplies the load that caused the refusal.
- `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)
