Skip to content

Address screening

One endpoint, for checking an address against our AML provider before you send money to it.

curl -X POST https://api.pay.nullswap.com/api/v1/aml/check-address \
  -H "x-api-key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "address": "0xfedcba0987654321fedcba0987654321fedcba09",
    "chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10"
  }'
{
  "amlResultId": "5c6d7e8f-9a0b-41c2-8d34-5e6f7a8b9c0d",
  "merchantId": "9f1d2c3b-4a5e-4f60-8b71-2c3d4e5f6a7b",
  "address": "0xfedcba0987654321fedcba0987654321fedcba09",
  "chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
  "status": "PASSED",
  "score": 12,
  "tags": [],
  "checkedAt": "2026-01-14T09:00:00.000Z",
  "fromCache": false
}
Field Type Required Notes
address string yes The address to screen, in the chain's native format
chainId uuid yes Which chain to interpret it on

Both matter. The same string of characters is a different address on different chains, and the same EVM address can come back with different verdicts on Ethereum and on BNB Chain.

Reading the verdict

status Meaning
PASSED Screened, and nothing disqualifying was found
FAILED Screened, and flagged
SKIPPED Not screened. The provider had no coverage, or screening did not apply

SKIPPED is not PASSED

It is the absence of an answer, not a clean one. Whether you treat it as acceptable is a policy decision — but it must be a decision. Writing status !== "FAILED" accepts every unscreenable address.

Field Notes
score Integer risk score. Higher is riskier. Meaningless when SKIPPED
tags Provider labels, e.g. ["mixer", "sanctions"]. Empty on a clean result
checkedAt When the screening ran — not when you called. Older than now on a cache hit
fromCache Whether this reused a recent result rather than re-screening
amlResultId Quote this if you want a verdict reviewed

Do not build your own thresholds on score. Ours already sits behind status, and the provider's scale is not something we hold fixed. Use status to decide and keep score and tags for your audit log.

Limits of this endpoint

Calling this does not screen an incoming payment

Payments are screened automatically, on their own path, and the result lands on the payment's amlStatus and amlResultId. This route is a separate, on-demand lookup that you drive. A PASSED here has no effect on any invoice or payment record, and cannot release a WITHHELD static-wallet deposit or turn a DIRTY balance CLEAN.

It also does not block payouts on its own. Nothing in POST /api/v1/merchant/withdrawals or in a refund consults it. If you want screening before you send money, you have to call this yourself and act on the answer.

When to call it

The main use is screening a destination address before you commit to it:

flowchart TD
    A["Customer submits a payout address"] --> B["POST /aml/check-address"]
    B --> C{"status"}
    C -- "PASSED" --> D["Save to the address book<br/>and proceed"]
    C -- "FAILED" --> E["Refuse, and tell the customer<br/>the address was rejected"]
    C -- "SKIPPED" --> F["Your policy decides"]
    D --> G["POST /merchant/withdrawals"]

Two concrete cases:

  • A refund address. POST .../refund records a refund you already sent — you cannot take it back. Screen the address while the customer is still on the page.
  • A withdrawal destination. Screen it once when it is added to your address book rather than on every payout.

Caching and cost

Repeated checks of the same address inside the provider's freshness window come back with fromCache: true and checkedAt pointing at the original run. That is cheaper and faster, but it also means a result can be hours old, and this endpoint gives you no way to force a fresh one.

Cache on your side too. There is no need to screen the same address on every page load: screening it when it is first supplied, and again if it has gone stale by your own standard, is enough.

Errors

Status message Cause
400 InvalidRequestBodyError address or chainId missing, or chainId is not a uuid
403 MerchantIsBlockedError The merchant is blocked
404 ChainWithIdNotFoundError Unknown chainId
422 EmptyAMLAddressError address was present but empty or whitespace
502 AMLServiceError The screening provider was unreachable or timed out

A 502 is not a verdict

AMLServiceError means the question was never answered — it is not a quiet SKIPPED. Retry it, and if the retry fails too, fall back to whatever you would do for an address you could not check rather than to whatever you do for a clean one.