Market Abuse
Scan a wallet for on-chain manipulation patterns, poll for the report, and tune the thresholds that decide what flags.
A market abuse scan reads a wallet's on-chain history and scores it against four manipulation patterns. Each detector produces a score, the bar it is compared against, and the detail of every signal it examined.
| Pattern | Key | Default threshold |
|---|---|---|
| Pump and dump | pump_dump | 60 |
| Insider trading | insider_trading | 70 |
| Smurfing | smurfing | 60 |
| Wash trading | wash_trading | 70 |
Scans are asynchronous. Launching one returns immediately with a job_id;
the report appears later, under that same id. There is no callback: poll the
result endpoint.
This page covers on-chain scans of a single wallet. To scan your own organization's order books instead, see Order-book abuse. Every endpoint is listed in the API reference.
Launching a scan
POST /v1/mabu/runs with a wallet and a chain:
curl -X POST https://monitoring.seqlense.com/api/v1/mabu/runs \
-H "Authorization: Bearer sq_..." \
-H "Content-Type: application/json" \
-d '{ "wallet": "0xd90e2f925da726b50c4ed8d0fb90ad053324f31b", "chain": "ethereum" }'The response carries the only identifier you need to keep:
{ "status": "ok", "job_id": "2966a324-2b67-48ee-a0e1-3fe6b123e1b2" }That job_id is both the run id and the scan_id its report is filed under.
chain accepts either the full name or the short code your wallets use, in any
case:
| Chain | Also accepted |
|---|---|
ethereum | eth |
bitcoin | btc |
solana | sol |
bsc | bnb |
polygon | matic |
arbitrum | arb |
optimism | op |
base |
Any other chain is refused by name, for example
Market abuse scans do not cover TRX - supported chains: .... The run always
stores the full name.
What else happens at launch:
- The scanned wallet is added to your organization's wallets if it is not already there. This is best effort and never blocks the scan.
- If the hand-off to the scanner fails, the call returns
400withRun created but dispatch failed: .... The run exists and is alreadyFAILED, with the reason indispatch_error.
The wallet must be at most 190 characters. An empty or non-JSON body returns
404, not 400.
Following a run
A run moves through a small set of states:
| Status | Meaning |
|---|---|
PENDING | created, not yet handed off |
DISPATCHED | handed off and running |
DONE | its report exists |
FAILED | the hand-off failed, or nothing was published within 10 minutes |
RUNNING and CANCELLED also exist in the model but are not set today.
GET /v1/mabu/runs/search lists your runs, newest first. Every filter is
optional and they combine with AND:
| Parameter | Effect |
|---|---|
status | one of the statuses above |
chain | full chain name only (ethereum, not eth) |
wallet | exact address, to list one wallet's scans |
q | prefix of a run id |
limit | page size, default 25, clamped to 1..200 |
offset | rows to skip, default 0 |
A filter with an unrecognised value is ignored rather than rejected, so check your spelling if a list looks too long.
curl "https://monitoring.seqlense.com/api/v1/mabu/runs/search?wallet=0xd90e2f925da726b50c4ed8d0fb90ad053324f31b&limit=5" \
-H "Authorization: Bearer sq_..."{
"runs": [
{
"id": "2966a324-2b67-48ee-a0e1-3fe6b123e1b2",
"wallet": "0xd90e2f925da726b50c4ed8d0fb90ad053324f31b",
"chain": "ethereum",
"status": "DONE",
"dispatch_error": "",
"created_at": "2026-09-20 10:12:03",
"started_at": "2026-09-20 10:12:04",
"finished_at": "2026-09-20 10:17:41"
}
],
"total": 1,
"limit": 5,
"offset": 0
}Timestamps are UTC, YYYY-MM-DD HH:MM:SS, and an empty string when not set
yet. GET /v1/mabu/runs/detail?id=... returns one run in the same shape; the
id must be a UUID.
Reading a report
GET /v1/mabu/runs/result?scan_id=... returns the report.
curl "https://monitoring.seqlense.com/api/v1/mabu/runs/result?scan_id=2966a324-2b67-48ee-a0e1-3fe6b123e1b2" \
-H "Authorization: Bearer sq_..."A 404 means the scan has not published anything yet. That is the normal
state of a run still in progress, not an error: poll until it answers. Every
30 seconds is plenty. If the run turns FAILED, stop.
Reading a report also settles the run: a run still in flight becomes DONE the
moment its report is found, and that is when an alert
is raised if something flagged. A 400 here means the report store could not
be reached; retry later.
The payload is the ingestion envelope; the report itself sits under data:
| Key | Contains |
|---|---|
scanId, wallet, blockchain, generatedAt, durationMs | identity of the scan |
fetch | how much history was read: analyzedTx, totalTxReported, historyCoveragePct, the window |
walletStats | age, tokens, counterparties, USD in and out |
graph | one-hop counterparties, and those filtered out as infrastructure |
riskCounterparties | mixers and phishing addresses met in the window |
tokens | per-token buys, sells and USD flows |
patterns | the four detectors, keyed by pattern |
summary | flaggedPatterns, anyFlag, maxConfirmedScore, patternScores, dataLimitations |
Start with data.summary, then open data.patterns for the detail. Each
pattern carries score, flagThreshold, flagged, scoringCoveragePct,
confidence, notes and its signals.
flagged and flagThreshold inside a pattern are the scanner's own
verdict, against its built-in bar. Your organization's verdict is score
compared with your threshold, and that is what alerting uses.
Signal statuses are the important part
Every detector reports a list of signals, and each signal has an id, a
label, points, evidence and one of three statuses:
| Status | Meaning |
|---|---|
confirmed | the signal fired, and its points count toward the score |
not_confirmed | assessed, and absent |
not_evaluable | could not be assessed at all |
not_evaluable is not a negative result. A signal that needs data the scan
could not obtain is reported as unanswerable rather than as clean, with a
reason naming what was missing. Read scoringCoveragePct and confidence on
a pattern to see how much of its scoring was reachable. Insider trading in
particular rarely reaches its threshold: it needs an event catalogue and a list
of project-team wallets that are usually unavailable.
Signals that are alternatives rather than additions share an exclusiveGroup
and count once toward the maximum.
Counterparties that are a finding in themselves
Mixers and phishing addresses are excluded from the counterparty graph so they
cannot inflate a score. They are listed separately under riskCounterparties,
with their entity and interaction count: excluding them from the scoring is not
the same as hiding them from you.
Thresholds
Each pattern flags when its score reaches a threshold between 0 and 100.
GET /v1/mabu/thresholds returns all four, with the platform default and
whether you have overridden it:
{
"thresholds": [
{ "pattern": "pump_dump", "label": "Pump & Dump", "default": 60, "threshold": 55, "customised": true },
{ "pattern": "insider_trading", "label": "Insider Trading", "default": 70, "threshold": 70, "customised": false },
{ "pattern": "smurfing", "label": "Smurfing", "default": 60, "threshold": 60, "customised": false },
{ "pattern": "wash_trading", "label": "Wash Trading", "default": 70, "threshold": 80, "customised": true }
]
}POST /v1/mabu/thresholds changes only the patterns you send and returns the
new list:
curl -X POST https://monitoring.seqlense.com/api/v1/mabu/thresholds \
-H "Authorization: Bearer sq_..." \
-H "Content-Type: application/json" \
-d '{ "thresholds": { "pump_dump": 55, "wash_trading": 80 } }'The whole body is checked before anything is saved. An unknown pattern, a
non-integer, or a value outside 0..100 returns 400 and changes nothing.
POST /v1/mabu/thresholds/reset drops every override. It takes no fields, but
the body must be JSON: send {}.
curl -X POST https://monitoring.seqlense.com/api/v1/mabu/thresholds/reset \
-H "Authorization: Bearer sq_..." \
-H "Content-Type: application/json" \
-d '{}'Thresholds do not change what the scanner computes, only what counts as a
flag. The bar in force when a run settles is the one that decides its alert;
runs already DONE are not re-evaluated.
Being told when something is found
A scan that flags raises an alert, which then travels through the notification channels your organization already configured.
| Source | market_abuse |
| Title | Market abuse: <worst pattern>, plus and N more when several flagged |
| Severity | HIGH, or CRITICAL when the worst pattern reaches 1.5x its threshold |
| Entity | the wallet, when your organization holds it |
| Metadata | scan_id, wallet, chain, every flagged pattern with its score and threshold, risk counterparties, and a link to the scan |
A clean scan raises nothing: an alert for every scan would train everyone to ignore them. Each scan raises at most one alert.
A channel that filters on specific sources will not receive these until
market_abuse is added to its list. A channel with no source filter receives
them straight away.
Scanning on a schedule
A wallet can carry a recurring scan, so you do not have to launch one yourself each time. Schedules live on the wallet, not on the scan:
curl -X POST https://monitoring.seqlense.com/api/v1/wallet/schedules \
-H "Authorization: Bearer sq_..." \
-H "Content-Type: application/json" \
-d '{ "wallet_id": "...", "kind": "MABU", "interval_days": 7, "enabled": true }'interval_days runs from 1 to 90. GET /v1/wallet/schedules?wallet_id=...
lists what a wallet has, and DELETE /v1/wallet/schedules?wallet_id=...&kind=MABU
removes one. A failed launch is kept in last_error and does not stop the
schedule. A wallet on a chain the scanner cannot read is refused when you
create the schedule.
Legacy scans (retro view)
If your organization used the previous market-abuse engine, its data stays readable, read-only, through three endpoints:
| Endpoint | Returns |
|---|---|
GET /v1/mabu/retro/scans | past scans; address= filters on one wallet (letters and digits only) |
GET /v1/mabu/retro/alerts | alerts the legacy engine raised |
GET /v1/mabu/retro/identities | identities it held |
Each takes limit (default 50, clamped to 1..200) and returns the legacy
payload as it comes: an org_id, a count, and a list named after the
endpoint (scans, alerts or identities) whose items keep the legacy
engine's own shape.
curl "https://monitoring.seqlense.com/api/v1/mabu/retro/scans?limit=20" \
-H "Authorization: Bearer sq_..."An organization with no legacy account gets a normal 200, not an error:
{ "org_id": null, "count": 0, "scans": [] }Legacy data belongs to the production workspace; a development key gets the
empty result. A 400 means the legacy engine could not be reached.
Limits worth knowing
- A scan reads a bounded window of the wallet's history, not all of it.
data.fetch.historyCoveragePctsays how much, anddata.summary.dataLimitationssays what the method could not evaluate. Read both before drawing a conclusion from a score. - A report is capped at 256 KiB; a trimmed one carries
_truncated. - Reports are passed through exactly as the scanner filed them, so they may carry fields not listed here.