Seqlense docs

Order-book abuse

Scan your organization's order flow for manipulation, client by client and book by book, and read the findings.

An order-book abuse run reads your organization's order events (placements, fills, cancels) and scores every client on every book against four manipulation patterns. Where an on-chain scan returns one verdict on one wallet, a run returns a finding per client, per book, per pattern.

PatternKeyDefault thresholdLooks for
Wash tradingwash_trading70a client trading with itself
Order lifecycleorder_lifecycle70spoofing and layering: orders placed to be cancelled
Rampingramping70accumulating, then pushing the price
Smurfingsmurfing60one order split into many small ones, including across venues

Runs are asynchronous. Launching one returns a run_id immediately; the report appears later under that id. There is no callback: poll the result endpoint.

Every endpoint is listed in the API reference.

Launching a run

POST /v1/obmabu/runs. Every field is optional, so the simplest run, all books of your organization over the automatic window, is an empty object:

curl -X POST https://monitoring.seqlense.com/api/v1/obmabu/runs \
  -H "Authorization: Bearer sq_..." \
  -H "Content-Type: application/json" \
  -d '{}'
{
  "status": "ok",
  "run_id": "0f9a1c2e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
  "window": { "from": "2026-09-01 00:00:00", "to": "2026-09-08 09:00:12" }
}

The body must be JSON even when you send no fields: {}. An empty body returns 404.

FieldMeaning
organizationthe order-data organization to scan; defaults to your own
venueone venue only, for example BYBIT; empty means every venue
instrumentone instrument only, for example ETH/USDC; empty means every instrument
from, tothe window, RFC 3339 (2026-09-01T00:00:00Z) or YYYY-MM-DD HH:MM:SS, UTC

venue and instrument are uppercased, at most 60 characters, and limited to letters, digits and / _ - .. to must be after from.

Leaving venue and instrument empty is the normal case. A run narrowed to one book cannot see an order split across venues, so it loses the cross-venue smurfing signal.

The organization defaults to the workspace of your key: a development key scans development data.

The window

The window actually used is echoed in the response (null on a side means unbounded):

You sendWindow
from and/or toused as given; a side you leave out is unbounded
neither, first run on this scopeunbounded: as much history as the scanner's 500 000-event budget allows
neither, a previous DONE run exists on the exact same scopeto = now, from = that run's end minus 24 hours, or 7 days ago if that reaches further back

The 24-hour overlap matches the longest pattern the scanner looks for, so behaviour straddling two runs is not cut in half. The 7-day floor keeps enough clients in each book for the comparisons to mean something. Scope matching is exact: a run on one instrument does not chain onto a run over every book.

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.

Following a run

StatusMeaning
PENDINGcreated, not yet handed off
DISPATCHEDhanded off and running
DONEits report exists
FAILEDthe hand-off failed, or nothing was published within 45 minutes

A run reads every book of an organization, so it takes longer than an on-chain scan; 45 minutes is the point where it is declared failed.

GET /v1/obmabu/runs/search lists your runs, newest first. Filters are optional and combine with AND; an unrecognised value is ignored.

ParameterEffect
statusPENDING, DISPATCHED, DONE or FAILED
organizationexact organization scanned
venueexact venue
qprefix of a run id
limitdefault 25, clamped to 1..100
offsetdefault 0, at most 100000

GET /v1/obmabu/runs/detail?id=... returns one run:

{
  "id": "0f9a1c2e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
  "organization": "acme",
  "venue": "BYBIT",
  "instrument": "",
  "scope": "acme · BYBIT",
  "window_from": "2026-09-01 00:00:00",
  "window_to": "2026-09-08 00:00:00",
  "status": "DONE",
  "dispatch_error": "",
  "created_by": 0,
  "created_at": "2026-09-08 09:00:12",
  "started_at": "2026-09-08 09:00:13",
  "finished_at": "2026-09-08 09:21:40"
}

An empty venue, instrument, window_from or window_to means "all" or "unbounded". created_by is 0 for a run launched with an API key.

Reading the report

curl "https://monitoring.seqlense.com/api/v1/obmabu/runs/result?run_id=0f9a1c2e-3b4d-4e5f-8a9b-0c1d2e3f4a5b" \
  -H "Authorization: Bearer sq_..."

A 404 means the run has not published anything yet, which is normal while it is DISPATCHED: poll, do not treat it as an error. It is also what you get for a run that is not your organization's. A 400 means the report archive could not be reached.

Reading the report settles the run: a run still in flight becomes DONE and alerts are raised at that moment.

The payload is the ingestion envelope; the report is under data:

KeyContains
runId, organization, generatedAt, durationMsidentity of the run
fetchwhat was read: books, events, orders, clients, and budgetExhausted if the 500 000-event budget ran out first
adequacywhat the feed contains: time resolution, event types, and how often optional fields are filled
booksone entry per book scanned, with totals
clientsone entry per client, with its books, accounts and per-book profile
findingsone entry per (client, book, pattern)
summaryanyFlag, flaggedCount, flaggedClients, byPattern, dataLimitations

Read the data limits first

data.adequacy and data.summary.dataLimitations come before the scores for a reason: they explain why a detector could not answer. For example, a feed with second-resolution timestamps cannot support spoofing signals measured in milliseconds, and a feed without best bid and ask cannot tell an order at the touch from one parked far away. dataLimitations states each gap as a sentence.

Findings

Each finding names a client and a book and carries the pattern's verdict:

{
  "pattern": "wash_trading",
  "title": "Wash trading",
  "clientId": "WASH01",
  "venue": "BYBIT",
  "instrument": "ETH/USDC",
  "profile": "RETAIL",
  "score": 100,
  "flagThreshold": 70,
  "flagged": true,
  "evaluableMaxScore": 100,
  "pointsNotEvaluable": 0,
  "scoringCoveragePct": 100,
  "confidence": 1,
  "notes": ["6 exact self-match(es) on ETH/USDC at BYBIT: identical price and quantity on both sides, within 60s."],
  "signals": [
    { "id": "self_trade_exact", "label": "The same client executed both sides...", "points": 50, "status": "confirmed", "reason": null, "evidence": { "matches": 6 } }
  ]
}

flagged and flagThreshold 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.

Signals use the same three statuses as on-chain scans:

StatusMeaning
confirmedfired, its points count toward the score
not_confirmedassessed, and absent
not_evaluablecould not be assessed; reason names the missing data

not_evaluable is never a clean result. A score built on half-assessable signals is a floor, not a measurement; scoringCoveragePct tells you how much of the method could be applied.

Profiles and baselines

Each client gets a profile per book: MARKET_MAKER, DIRECTIONAL, RETAIL or DORMANT, assigned by heuristic or declared. The profile only chooses which population of clients the client is compared against. It never takes anyone out of scope: a market maker can manipulate, and a self-trade flags whoever does it.

Several order-lifecycle signals are scored against the book's population rather than a fixed limit, since there is no universal order-to-trade ratio. Their evidence shows the population median, the bar derived from it, the population size and the absolute floor next to the client's own value.

Prices and quantities in the report are strings. Keep them as strings or parse them as decimals: converting to floating point rounds exact figures.

Thresholds

Same model as on-chain scans, with its own four patterns. GET /v1/obmabu/thresholds returns each pattern's threshold, default and whether it is customised.

POST /v1/obmabu/thresholds changes only the patterns you send and returns the new list:

curl -X POST https://monitoring.seqlense.com/api/v1/obmabu/thresholds \
  -H "Authorization: Bearer sq_..." \
  -H "Content-Type: application/json" \
  -d '{ "thresholds": { "order_lifecycle": 65 } }'

An unknown pattern or a non-integer returns 400 and nothing is saved. Unlike on-chain thresholds, an integer outside 0..100 is clamped into range rather than refused, so 150 is stored as 100.

POST /v1/obmabu/thresholds/reset restores every default. Send {} as the body:

curl -X POST https://monitoring.seqlense.com/api/v1/obmabu/thresholds/reset \
  -H "Authorization: Bearer sq_..." \
  -H "Content-Type: application/json" \
  -d '{}'

Alerts

A run that flags raises alerts through your notification channels, one per (client, book), with that pair's flagged patterns folded into it.

Sourceorderbook_abuse
Entity typeOB_CLIENT
TitleOrder-book abuse: <client> on <venue> <instrument>
SeverityHIGH, or CRITICAL when the worst pattern reaches 1.5x its threshold
Metadatarun id, client, book, profile, every flagged pattern with score, threshold, scanner threshold and coverage, the not_evaluable reasons, the feed's limitations, and a link to the run

The title is identical from one run to the next, so ongoing behaviour does not pile up: while an alert on a client and book is open or acknowledged, later runs do not raise it again. Once it is resolved, a recurrence fires anew.

A run raises at most 25 individual alerts. Any further subjects are listed in a single digest alert, Order-book abuse: N more clients flagged in one run. So many flags usually means a threshold needs revisiting. A clean run raises nothing.

A channel that filters on specific sources will not receive these until orderbook_abuse is added to its list.

On this page