Seqlense docs

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.

PatternKeyDefault threshold
Pump and dumppump_dump60
Insider tradinginsider_trading70
Smurfingsmurfing60
Wash tradingwash_trading70

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:

ChainAlso accepted
ethereumeth
bitcoinbtc
solanasol
bscbnb
polygonmatic
arbitrumarb
optimismop
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 400 with Run created but dispatch failed: .... The run exists and is already FAILED, with the reason in dispatch_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:

StatusMeaning
PENDINGcreated, not yet handed off
DISPATCHEDhanded off and running
DONEits report exists
FAILEDthe 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:

ParameterEffect
statusone of the statuses above
chainfull chain name only (ethereum, not eth)
walletexact address, to list one wallet's scans
qprefix of a run id
limitpage size, default 25, clamped to 1..200
offsetrows 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:

KeyContains
scanId, wallet, blockchain, generatedAt, durationMsidentity of the scan
fetchhow much history was read: analyzedTx, totalTxReported, historyCoveragePct, the window
walletStatsage, tokens, counterparties, USD in and out
graphone-hop counterparties, and those filtered out as infrastructure
riskCounterpartiesmixers and phishing addresses met in the window
tokensper-token buys, sells and USD flows
patternsthe four detectors, keyed by pattern
summaryflaggedPatterns, 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:

StatusMeaning
confirmedthe signal fired, and its points count toward the score
not_confirmedassessed, and absent
not_evaluablecould 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.

Sourcemarket_abuse
TitleMarket abuse: <worst pattern>, plus and N more when several flagged
SeverityHIGH, or CRITICAL when the worst pattern reaches 1.5x its threshold
Entitythe wallet, when your organization holds it
Metadatascan_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:

EndpointReturns
GET /v1/mabu/retro/scanspast scans; address= filters on one wallet (letters and digits only)
GET /v1/mabu/retro/alertsalerts the legacy engine raised
GET /v1/mabu/retro/identitiesidentities 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.historyCoveragePct says how much, and data.summary.dataLimitations says 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.

On this page