API reference
The classification API
https://www.cryptoornot.com/apiEarly access tier · live
- Live now at the base URL above: health, classify, classify/batch, assessment, calibration, and registry summary.
- No API key. 60 requests per minute per IP address.
- It runs the same engine as the self-serve assessment, a TypeScript port held identical to the classifier service by a parity test.
- Keyed access, higher limits, the monitoring endpoints (
/v1/monitor/*,/v1/feed/registry), webhook delivery, and the Unit21 alerts adapter run on the full service, which we set up for design partners. See Full service.
For a key or a design partnership, email contact@cryptoornot.com.
Try it · POST /api/v1/classify
Try a legal name such as FORIS DAX INC, ELECTRIC SOLIDUS INC or RED LEAF CHICAGO LLC, or a decoy such as PAYPAL INC.
Access and limits
No key, 60 requests per minute
The early access tier needs no authentication. Requests are counted per IP address over a sliding 60 second window. Over the limit, the API returns 429 with a Retry-After header. The counter is held in memory on each server instance, so treat the limit as best effort, not a quota.
Every response carries an X-Request-ID header. Send your own and it is echoed back; otherwise one is generated. CORS is open (*), so you can call the API from a browser.
| Requests | 60 per minute per IP address, sliding window |
| classify text | 2,000 characters |
| classify/batch items | 1,000 per request |
| assessment file | 4 MB of UTF-8 CSV, 500,000 data rows; the whole request body under 4.5 MB |
| API key | None on this tier |
Over-limit response
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
{"detail":"Rate limit exceeded; retry after the indicated interval."}Endpoints
Six endpoints on the early access tier
Every example response on this page is real output of this API, captured with curl. Long responses are abbreviated where marked with ....
/v1/healthLiveness, plus the versions that decide what a classification means: the registry version and the calibration table version, including whether the calibration is synthetic. Not rate limited.
Response 200
{
"status": "ok",
"registry_version": "0.4.1",
"calibration_version": "0.4.1+2026-10-10",
"calibration_dataset_kind": "synthetic"
}/v1/classifyClassify one counterparty name or raw payment descriptor. Send the text exactly as your system rendered it: a wire ORIG/BNF block, an ACH descriptor, a card settlement line, or a name. Give the rail if you know it. The rail picks the grammar used to parse the text before matching.
| Parameter | In | Required | Description |
|---|---|---|---|
| text | body | yes | Counterparty name or raw payment text, up to 2,000 characters. |
| rail | body | no | "wire", "ach", "card", or "unknown" (default). Other values are treated as unknown. |
Request
curl -X POST "https://www.cryptoornot.com/api/v1/classify" \
-H "Content-Type: application/json" \
-d '{"text": "ATM OPS INC", "rail": "ach"}'Response 200 · crypto
{
"verdict": "crypto",
"confidence": 0.978432,
"score": 100,
"strategy": "exact",
"entity_id": "bitstop",
"entity_name": "Bitstop",
"categories": [
"kiosk_operator"
],
"evidence": {
"observable_type": "legal_name",
"observable_value": "ATM Ops Inc",
"observable_confidence": 0.98,
"source_url": "https://msb.fincen.gov/retrieve.msb.list.php",
"observed_at": "2026-08-08",
"method": "FinCEN MSB registrant list, Coral Gables FL, 1473 branches, refreshed 2026-06-02; re-checked 2026-09-19 against the full FinCEN MSB registrant list (OpenSanctions mirror us_fincen_msb): registrant present",
"review_status": "reviewed"
},
"matched_field": {
"field": "raw_text",
"source": "raw",
"text": "ATM OPS INC"
},
"calibration": {
"bin_key": "exact|all",
"bin_samples": 625,
"bin_precision": 0.9984,
"observable_confidence": 0.98,
"table_version": "0.4.1+2026-10-10",
"dataset": "datasets/labeled_descriptors.csv",
"dataset_kind": "synthetic",
"synthetic": true
},
"rail": "ach",
"grammar": "ach.plain",
"normalized_input": "ATM OPS INC",
"note": ""
}Response 200 · needs_review ({"text": "BLOCK INC", "rail": "wire"})
{
"verdict": "needs_review",
"confidence": 0.5990399999999999,
"score": 100,
"strategy": "exact",
"entity_id": "block-cash-app",
"entity_name": "Cash App (Block)",
"categories": [
"broker"
],
"evidence": {
"observable_type": "legal_name",
"observable_value": "Block, Inc.",
"observable_confidence": 0.6,
"source_url": "https://www.dfs.ny.gov/virtual_currency_businesses",
"observed_at": "2026-09-12",
"method": "NYDFS Virtual Currency Businesses regulated-entity list (BitLicense and limited purpose trust charter holder) Confidence capped: the bare word is ambiguous.; source re-checked 2026-09-19: value present verbatim",
"review_status": "reviewed"
},
"matched_field": {
"field": "raw_text",
"source": "raw",
"text": "BLOCK INC"
},
"calibration": {
"bin_key": "exact|all",
"bin_samples": 625,
"bin_precision": 0.9984,
"observable_confidence": 0.6,
"table_version": "0.4.1+2026-10-10",
"dataset": "datasets/labeled_descriptors.csv",
"dataset_kind": "synthetic",
"synthetic": true
},
"rail": "wire",
"grammar": "wire.untagged",
"normalized_input": "BLOCK INC",
"note": "Below the auto-classification threshold (0.92); route to an analyst."
}Response 200 · no_match ({"text": "HARBORVIEW LOGISTICS INC", "rail": "ach"})
{
"verdict": "no_match",
"confidence": 0,
"score": 0,
"strategy": null,
"entity_id": null,
"entity_name": null,
"categories": [],
"evidence": null,
"matched_field": null,
"calibration": null,
"rail": "ach",
"grammar": "ach.plain",
"normalized_input": "HARBORVIEW LOGISTICS INC",
"note": "No registry match. Absence of a match is not evidence the counterparty is unrelated to crypto; the registry does not claim complete coverage."
}/v1/classify/batchClassify up to 1,000 items in one request. Each item takes the same fields as /v1/classify. The response is an array of classification objects in input order.
| Parameter | In | Required | Description |
|---|---|---|---|
| items | body | yes | Array of {text, rail} objects, at most 1,000. |
Request
curl -X POST "https://www.cryptoornot.com/api/v1/classify/batch" \
-H "Content-Type: application/json" \
-d '{"items": [
{"text": "FORIS DAX INC", "rail": "wire"},
{"text": "PAYPAL INC", "rail": "wire"}
]}'Response 200 · abbreviated
[
{
"verdict": "crypto",
"confidence": 0.968448,
"score": 100,
"strategy": "exact",
"entity_id": "crypto-com-us",
"entity_name": "Crypto.com (US)",
"categories": [
"exchange",
"broker"
],
"...": "evidence, matched_field, calibration, rail, grammar, normalized_input, note"
},
{
"verdict": "needs_review",
"confidence": 0.7960999999999999,
"score": 100,
"strategy": "fuzzy",
"entity_id": "paypal-crypto",
"entity_name": "PayPal and Venmo crypto",
"categories": [
"broker",
"custodian"
],
"...": "evidence, matched_field, calibration, rail, grammar, normalized_input, note"
}
]/v1/assessmentThe Exposure Assessment: a transactions CSV in, a classified ledger and an assessment report out. Send it as multipart form data (the same shape as the full service) or as JSON. Columns are mapped by common aliases, and the mapping used is reported back. Rows that cannot be read become recorded issues, not silent drops. The file is processed in memory and never stored.
| Parameter | In | Required | Description |
|---|---|---|---|
| file | form | yes* | The transactions CSV. UTF-8, at most 4 MB and 500,000 data rows. |
| rail | form or body | no | Rail for rows without their own channel column. Same values as classify. |
| csv | body (JSON) | yes* | The CSV as a string, instead of a file upload. |
| filename | body (JSON) | no | Name shown in the report. Default upload.csv. |
| format | query | no | "json" (default) returns {report, ledger}. "markdown" returns the report document. "csv" returns the classified ledger. |
* file for multipart requests, csv for JSON requests.
Request
# Multipart, like the full service
curl -X POST "https://www.cryptoornot.com/api/v1/assessment?format=json" \
-F "file=@sample_transactions.csv" \
-F "rail=unknown"
# Or JSON
curl -X POST "https://www.cryptoornot.com/api/v1/assessment?format=markdown" \
-H "Content-Type: application/json" \
-d '{"csv": "description,amount\nPAYWARD INC,1200.00\n", "filename": "march.csv", "rail": "wire"}'Response 200 · format=json, sample file, abbreviated
{
"report": {
"title": "Digital asset exposure assessment",
"source": {
"filename": "sample_transactions.csv",
"rows_read": 25,
"transactions_classified": 25,
...
},
"registry_version": "0.4.1",
"registry_entity_count": 127,
"totals": {
"crypto": {
"transactions": 13,
"amount": 5944770
},
"needs_review": {
"transactions": 7,
"amount": 17190.5
},
"no_match": {
"transactions": 5,
"amount": 860.92
}
},
"by_entity": [
{
"entity_id": "paxos",
"entity_name": "Paxos",
"categories": [
"stablecoin_issuer",
"custodian",
"bank"
],
"transactions": 1,
"amount": 2400000,
"max_confidence": 0.9784,
"min_confidence": 0.9784,
"verdicts": {
"crypto": 1
},
"sources": [
"https://paxos.com/legal"
]
},
...
],
"...": "by_category, review_queue, calibration, limitations"
},
"ledger": [
{
"transaction": {
"row_number": 2,
"transaction_id": "TXN-1001",
"posted_at": "2026-07-02",
"amount": "254000.00",
"currency": "USD",
"direction": "debit",
"rail": "wire",
"descriptor": "ORIG:PAYWARD VENTURES INC 548 MARKET ST SAN FRANCISCO BNF:FIRST HORIZON CUSTOMER AC-4419820011 REF:88213"
},
"classification": {
"verdict": "crypto",
"confidence": 0.968448,
"score": 100,
"strategy": "exact",
"entity_id": "kraken",
"entity_name": "Kraken",
"categories": [
"exchange"
],
"evidence": {
"observable_type": "legal_name",
"observable_value": "Payward Ventures, Inc.",
"observable_confidence": 0.97,
"source_url": "https://www.kraken.com/legal",
"observed_at": "2026-08-01",
"method": "public-record review",
"review_status": "reviewed"
},
"matched_field": {
"field": "originator",
"source": "parsed",
"text": "PAYWARD VENTURES INC 548 MARKET ST SAN FRANCISCO"
},
"calibration": {
"bin_key": "exact|parsed|all",
"bin_samples": 625,
"bin_precision": 0.9984,
"observable_confidence": 0.97,
"table_version": "0.4.1+2026-10-10",
"dataset": "datasets/labeled_descriptors.csv",
"dataset_kind": "synthetic",
"synthetic": true
},
"rail": "wire",
"grammar": "wire.tagged",
"normalized_input": "ORIG PAYWARD VENTURES INC 548 MARKET ST SAN FRANCISCO BNF FIRST HORIZON CUSTOMER AC 4419820011 REF 88213",
"note": ""
}
},
...
]
}Response 200 · format=markdown, first lines
# Digital asset exposure assessment
Source file: `sample_transactions.csv`
Generated: 2026-10-10T06:57:10+00:00
Registry: v0.4.1 (127 entities)
Calibration: v0.4.1+2026-10-10 (synthetic, fitted 2026-10-10)
> The confidence values in this report come from a calibration fitted on synthetic data, not on labeled bank transactions. Read the limitations section before acting on any number here.
## Summary
| Verdict | Transactions | Amount |
| --- | ---: | ---: |
| Crypto counterparty | 13 | 5,944,770.00 |
| Needs review | 7 | 17,190.50 |
| No registry match | 5 | 860.92 |
25 of 25 rows were classified. 0 row-level issues were recorded during ingestion.
...Response 200 · format=csv, first rows
row_number,transaction_id,posted_at,amount,currency,direction,rail,descriptor,verdict,confidence,entity_id,entity_name,categories,matched_field,matched_text,strategy,raw_score,observable_type,observable_value,source_url,observed_at,review_status,calibration_bin,calibration_samples,note
2,TXN-1001,2026-07-02,254000.00,USD,debit,wire,ORIG:PAYWARD VENTURES INC 548 MARKET ST SAN FRANCISCO BNF:FIRST HORIZON CUSTOMER AC-4419820011 REF:88213,crypto,0.9684,kraken,Kraken,exchange,originator,PAYWARD VENTURES INC 548 MARKET ST SAN FRANCISCO,exact,100.0,legal_name,"Payward Ventures, Inc.",https://www.kraken.com/legal,2026-08-01,reviewed,exact|parsed|all,625,
3,TXN-1002,2026-07-02,1450.00,USD,debit,ach,COINBASE INC DES:PURCHASE ID:9938271004 INDN:JOHN R SMITH CO ID:1455293997 WEB,crypto,0.9884,coinbase,Coinbase,exchange|custodian|broker,company_name,COINBASE INC,exact,100.0,legal_name,"Coinbase, Inc.",https://www.coinbase.com/legal/user_agreement/united_states,2026-08-01,reviewed,exact|parsed|all,625,
.../v1/calibrationThe full calibration table behind every confidence: per-bin sample counts and precision, the verdict thresholds and the policy choices behind them, holdout metrics, and the caveat. It is published so the number can be checked.
Response 200 · abbreviated
{
"version": "0.4.1+2026-10-10",
"generated_at": "2026-10-10",
"dataset": "datasets/labeled_descriptors.csv",
"dataset_kind": "synthetic",
"synthetic": true,
"thresholds": {
"crypto": 0.91586,
"review": 0.348375,
"target_precision": 0.9,
"target_met": true,
"review_min_yield": 0.3333,
"f2_optimal_review_reference": 0.1564,
"note": "target_precision and review_min_yield are policy choices about consequences, not measurements. The thresholds derived from them are the lowest calibrated confidences that satisfy them on the calibration fold. f2_optimal_review_reference is reported for comparison only: it is prevalence-sensitive and this set's prevalence is fabricated."
},
"holdout_fold": {
"rows": 3760,
"positives": 1848,
"auto_crypto": 505,
"auto_crypto_correct": 505,
"precision_at_crypto": 1,
"recall_at_crypto": 0.2733,
"needs_review": 1129,
"needs_review_correct": 859,
"review_queue_yield": 0.7609,
"recall_including_review": 0.7381,
"missed_entirely": 465
},
"caveat": "Fitted on synthetic descriptors generated by scripts/build_dataset.py, at a crypto prevalence chosen by this project rather than observed in a bank file. Precision depends on prevalence, so these confidences are optimistic by an unknown margin and are not a field accuracy claim. Refit on labeled bank data before relying on a confidence for an exposure decision.",
"...": "bins, calibration_fold, smoothing"
}/v1/registry/summaryWhat coverage a verdict was measured against: registry version, entity and observable counts, counts by category, observable type and review status, and every entity by id, name and category. Observables and their sources are not published on this tier. The registry page shows the same numbers.
Response 200 · abbreviated
{
"version": "0.4.1",
"generated_at": "2026-09-19",
"entity_count": 127,
"observable_count": 345,
"entities_by_category": {
"bank": 8,
"broker": 31,
"custodian": 28,
"exchange": 21,
"infrastructure": 15,
"kiosk_operator": 26,
"miner": 12,
"otc_desk": 11,
"payment_processor": 14,
"stablecoin_issuer": 11
},
"observables_by_type": {
"alias": 33,
"dba": 40,
"legal_name": 272
},
"observables_by_review_status": {
"disputed": 18,
"reviewed": 322,
"unreviewed": 5
},
"entities": [
{
"id": "abra",
"display_name": "Abra",
"categories": [
"broker",
"otc_desk"
]
},
{
"id": "alpaca-crypto",
"display_name": "Alpaca Crypto",
"categories": [
"broker"
]
},
...
]
}Errors
Error responses
Errors are JSON with a detail field that says what went wrong. Malformed input always gets a 4xx with a reason.
| Status | When |
|---|---|
| 400 | The body is not valid JSON, the multipart body is malformed, or the uploaded file is not UTF-8. |
| 413 | The body or the uploaded file is over its size limit. Split the file and send the parts separately. |
| 415 | The assessment request is neither multipart/form-data nor JSON. |
| 422 | The request fails validation (missing text, more than 1,000 batch items, text over 2,000 characters, an unknown format), or the CSV cannot be ingested: no payment text column, more than 500,000 rows, or a CSV parse error. The detail field says which. |
| 429 | Over the rate limit. The Retry-After header gives the seconds to wait. |
| 500 | A fault on our side. The body never echoes your input. Quote the X-Request-ID header when you report it. |
Example · 1,001 batch items
HTTP/1.1 422 Unprocessable Entity
{"detail":"items holds 1,001 entries; the limit is 1,000 per request."}Reading a verdict
What the three verdicts mean
crypto
A registry entity matched with a calibrated confidence above the auto-classify threshold. The response includes the matched observable and its source URL, observed date, method, and review status.
needs_review
The evidence is real but not strong enough to act on without a person. Route it to an analyst with the evidence in the response. The analyst makes the call.
no_match
No registry entity matched. This does not prove the counterparty is unrelated to crypto. The registry does not claim complete coverage, and the response says so.
Every confidence carries one caveat: the calibration table is fitted on synthetic payment descriptors, because no institution has labeled a real set with us yet. Treat confidences as a working prior, not a field accuracy claim. GET /v1/calibration publishes the whole basis.
Full service · design partners
Monitoring, feeds, and delivery
The classifier service (Python, FastAPI) adds the layers a bank needs after the first assessment. It is not publicly hosted. We set it up for each design partner. Paths below are relative to that deployment.
- Keyed access: every
/v1endpoint requires anX-API-Keyheader. The default limit is 120 requests per minute per key, configurable per deployment. - Monitoring state (watched book, events, webhooks) is scoped to each API key.
- The same classify, batch, assessment (up to 25 MB), and calibration endpoints, plus an entity list with observable counts at
/v1/entities.
Monitoring endpoints
| POST | /v1/monitor/counterparties | Register a counterparty book. Each item is classified now and watched from then on. |
| GET | /v1/monitor/counterparties | List the watched book. |
| DELETE | /v1/monitor/counterparties/{id} | Stop watching one entry. |
| POST | /v1/monitor/transactions | Fold observed transactions into the watch state. |
| POST | /v1/monitor/rescan | Re-run the whole book against the current registry. A counterparty that was no_match becomes an exposure the day the registry learns its name. |
| GET | /v1/monitor/summary | Verdict counts, exposure count, and the current feed cursor. |
| GET | /v1/monitor/events | The event feed from a cursor (since, limit, format). Formats: json, jsonl, csv, unit21, actimize-draft. |
| GET | /v1/monitor/adapters | The delivery formats, with descriptions. |
| POST | /v1/monitor/webhooks | Subscribe an HTTPS endpoint to events, signed with HMAC-SHA256. |
| GET | /v1/monitor/webhooks | List subscriptions. Secrets are masked. |
| DELETE | /v1/monitor/webhooks/{id} | Remove a subscription. |
| GET | /v1/monitor/webhooks/{id}/deliveries | Every recorded delivery attempt for a subscription. |
| GET | /v1/feed/registry | The registry changelog: entities and observables added, removed, and updated between versions, each with its provenance. |
Event types
exposure_detected: A watched counterparty classified as crypto, first time or on a change into crypto.verdict_changed: Any verdict transition, with the before and after states and the registry version behind each.review_required: A counterparty classified needs_review.exposure_cleared: A counterparty that was crypto no longer matches. The coverage caveat travels in the payload.registry_updated: The service saw a new registry version. The payload carries the delta.
Every event that carries a confidence also carries the synthetic-calibration caveat.
Delivery
- Pull feed. The event id is the cursor. Pass the last id you processed as
since. The next cursor is in theX-CryptoOrNot-Next-Cursorheader. - Webhooks. Each delivery POSTs one event with
X-CryptoOrNot-Event-IdandX-CryptoOrNot-Signature, an HMAC-SHA256 of the raw body formattedsha256=<hex>. There is no retry queue yet. Every attempt is recorded, and the pull feed is the recovery path. - Unit21. The
unit21format emits one create-alert request body per event for Unit21'sPOST /v1/alerts/create. It is verified against Unit21's published API reference and pinned by contract tests. A push client handles Unit21 auth, idempotency, retries, and rate limits. It has not yet run against a live Unit21 organization. - NICE Actimize. The
actimize-draftformat is a draft, alert-shaped mapping labeled"mapping_status": "draft". Actimize publishes no ingestion API, so it stays a draft until a design partner supplies the specification.
Verify a webhook signature (Python)
import hashlib, hmac
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
assert hmac.compare_digest(expected, request.headers["X-CryptoOrNot-Signature"])Agent tools
The classifier as an MCP server
For AML platforms whose agents triage alerts, the classifier is also a Model Context Protocol server over stdio. The tools call the same library code as the HTTP service, so an agent gets the same classification, evidence, and confidence. Every payload with a confidence carries the synthetic-calibration caveat.
The stdio server is local and single-tenant: the monitoring tools work on one default tenant in a local database, not on the per-key state of a running HTTP service.
classify_descriptor
Classify one payment text or name. Returns the verdict with the full evidence chain, the same result as POST /v1/classify.
classify_batch
Classify up to 1,000 items in one call, in input order.
registry_summary
Registry version, entity and observable counts, entities per category, and observables per review status.
lookup_entity
One registry entity by id or name, with every observable, its provenance, and banking relationships (context only, never matched).
watch_counterparties
Register a counterparty book for monitoring and classify it now.
exposure_events
Read the monitoring event feed from a cursor.
registry_changes
The registry changelog, version by version, with provenance.
monitor_summary
Watch state: watched count, verdict counts, exposure count, feed cursor.
MCP client configuration
{
"mcpServers": {
"crypto-or-not": {
"command": "uv",
"args": [
"run", "--directory",
"/path/to/crypto-or-no-crypto",
"cryptoornot-mcp"
]
}
}
}The server runs from the service repository with uv run cryptoornot-mcp. Access comes with a design partner or early access engagement. Email contact@cryptoornot.com to set it up.