Skip to main content
A screening policy is stored as data, not as code. The policy editor in the developer console writes one shape of rule. This page describes the full format, which you author through the ScreeningPolicies endpoints in the API reference. Use it when you need a condition the editor cannot express: a negation, a nested combination of conditions, or a screening provider field the editor does not offer.

Prerequisites

  • Your own provider key for TRM Labs or Chainalysis. See bring your own screening key.
  • The Admin role on the environment.
  • Screening policies enabled for the environment. They are in Beta.

Shape

A policy holds two ordered lists of rules:
preScreen decides whether the provider is called. postScreen decides the verdict from the provider’s response. Each list holds at most 200 rules. Both are optional, and a policy with neither behaves exactly like an environment with no policy. Every rule is one condition and one action:
Rules are evaluated top to bottom and the first match wins. The list order is the policy, so Dynamic stores your rules exactly as you send them and never reorders them.

Actions

then in preScreen is SCREEN or SKIP. If no pre-screen rule matches, the address is screened. then in postScreen is BLOCK, ALERT, or ALLOW. If no post-screen rule matches, the address is allowed.

Conditions

A condition is one of these. Each object holds exactly one form. all and any take 1 to 200 conditions. Conditions nest up to 5 levels deep.

Comparing one field

op is one of eq, neq, in, nin, gte, lte, gt, lt, or contains.
  • in and nin take an array of 1 to 200 values. Every other operator takes a single value.
  • contains is a substring test. It is never a regular expression.
Which operators a field accepts depends on what the field holds. A pairing the field does not accept is rejected with 400. Ordering operators do not work on free text. contains works only on free text.

Quantifying over a provider array

Both providers report signals as arrays. exists matches when one element of the array satisfies every condition in where:
Every condition inside where binds to the same element. That is the point of exists. Two unrelated signals cannot jointly satisfy one rule, so the example above matches an address carrying a high-scoring Mixer signal, not an address that carries a Mixer signal and separately carries something else scoring 10. Inside where, a bare field name such as category reads the bound element. An absolute path such as chainalysis.risk still reads the response.

Matching every screen

{ "always": true } is how you turn a surface off:
  • It is accepted only in preScreen, and only as a rule’s whole condition. Nested anywhere, or used in postScreen, it is a 400.
  • false is a 400. The form exists to match, not to be switched off.
  • Nothing may follow it. A later rule is unreachable and the write is rejected.
Skipping the provider call drops that provider’s own sanctions blocks at that surface. Dynamic still checks every address against its own sanctions list. See when to screen.

Fields

Both phases, both providers

tx.amountUsd, tx.asset, chain, and address. These need no provider response, so they work in both phases, and they are the only fields a preScreen rule may use.
  • address is compared exactly, with no case folding, because Base58 and Base32 chains are case significant.
  • chain is compared exactly against the chain Dynamic recorded for the wallet, such as EVM or SOL.
  • tx.asset is compared case-insensitively, so usdc matches USDC.
  • tx.amountUsd and tx.asset are present only where the request established them. Neither is available on the signing path.

TRM Labs

in for an exists is trm.addressRiskIndicators or trm.entities. riskType is OWNERSHIP, COUNTERPARTY, or INDIRECT. Category strings are compared case sensitively, in the TitleCase TRM returns, such as Mixer. Risk score levels are numbers and are compared numerically.

Chainalysis

in for an exists is chainalysis.addressIdentifications, chainalysis.exposures, or chainalysis.triggers. chainalysis.risk is an ordered level, so gte works on it: Low, Medium, High, Severe. category, chainalysis.cluster.category, direction, and the ruleTriggered fields are compared case-insensitively. chainalysis.cluster.name and chainalysis.riskReason are compared exactly. cluster and ruleTriggered are nullable, so a field beneath either reads as unknown when the provider omits the parent.
exposureType has a closed vocabulary of direct and indirect, lowercase. The developer console labels the same distinction Direct exposure and Indirect exposure, so do not copy a label into a rule as a value.

Unknown values

A field the response does not carry is unknown, not absent and not false. A rule fires only on a definite match.
  • A condition on an unknown field does not match, and it does not make the rule match either.
  • not, neq, and nin do not fire on an unknown value. They are not a way to test for absence.
  • exists over an empty array does not match. exists over a field the response omits is unknown.
This is what makes amount conditions safe to write on a surface that sometimes has no amount: the condition sits out rather than firing on a value that was never established.

Writing a policy

A write replaces the whole rule blob for one provider’s sign-in policy or transaction policy.
  • Send revision exactly as the read returned it. A stale revision is rejected with 409 rather than applied, so a concurrent edit cannot be silently overwritten.
  • Omit revision only when creating that policy for the first time.
  • Rules are validated before they are stored. A field the provider’s schema does not define, an operator the field does not accept, an operand of the wrong type, a list longer than 200, and a condition nested too deep are each a 400.
  • A TRM policy naming a chainalysis.* field is a 400, and the reverse is too. The provider comes from the path, and the rules must match it.
  • A saved policy is live everywhere within 60 seconds.
Neither the sanctions baseline nor the trailing allow appears in rules. Both are generated on every screen, so there is nothing to read back and no request body that can weaken them. The API calls the baseline the sanctions floor.

Rules the policy editor writes

The policy editor writes ordinary rules in this format. A rule you send in the same shape is shown in the editor as editable, not as an advanced rule. See Configure a screening policy for the editor itself.

When to screen

Each When to screen control is stored as a SKIP rule on everything it excludes. Screening only Solana, and only transfers of $1,000 or more:

A category rule

A category rule is one exists over the provider’s signal array. Blocking Mixer on the address itself at a TRM Labs risk score of 10 (high) or above:
A category rule with more than one threshold is stored as one rule per threshold. The editor stores the stricter action first, and a higher score before a lower one, whatever order the thresholds were added in.

How the policy editor treats an API-authored rule

The policy editor recognizes a rule by its structure. A rule outside the shape it writes is shown read-only, at its real position in the evaluation order, with a delete control.
  • Saving from the policy editor does not rewrite an advanced rule and does not move it.
  • An advanced rule does not disable the editor. Rules the editor does recognize stay editable.
  • Reading a policy and saving it back unchanged leaves it unchanged.

Address exceptions

An address exception is an entry on the provider, not a rule in the policy. It applies to both the sign-in policy and the transaction policy unless you narrow it, and each entry is written on its own:
  • disposition is DENY to block the address or ALLOW to exempt it from your rules.
  • Omit chain to apply the entry to every chain.
  • Omit appliesToScope to apply the entry to both policies.
  • An address already listed for that provider and chain is rejected with 409. Remove the existing entry to change its decision.
  • An exempted address is still blocked if it is sanctioned. The sanctions baseline is evaluated before address exceptions.

Screening an address yourself

Two endpoints return a screening verdict on demand. Both are policy aware, and both gained an additive action of allow, alert, or block alongside the field they already returned. An alert never blocks, so an integration that reads only isBlocked or decision behaves exactly as it did before policies existed. They take different inputs, so which one you want follows from what you have: Reach for the POST before signing from a wallet you do not control. Do not build it out of the GET: extracting destinations from a raw payload means reimplementing per-chain decoding for EVM, Solana, Bitcoin, Tron, Sui, and Stellar, and the verdict over a set of addresses is not the same guarantee as a verdict over each one in turn.
The wallet/sanctions path predates screening policies. The endpoint is policy aware and returns the full allow, alert, or block verdict, not a sanctions yes or no. The path is kept because it is released.
On GET /sdk/{environmentId}/wallet/sanctions:
  • scope chooses which policy to evaluate, and defaults to TRANSACTION. This is the only surface where the caller chooses the policy. Every other surface always uses the policy listed for it on screening policies.
  • amountUsd and asset supply values to whichever policy scope selected. They are independent of scope: passing an amount does not move you to the transaction policy. They also never trigger a provider call and are not part of the cache key, so screening an address as a $10,000 transfer is free.
  • applyPolicies: false returns the baseline verdict alone. It does not reduce the number of provider calls.
On POST /sdk/{environmentId}/wallet/screenTransaction:
  • The request carries the transaction payload and takes no scope, no amount, and no policy bypass. It always evaluates the transaction policy. This is what an SDK blocks a signature on, so it accepts nothing from the caller that could weaken the verdict.
  • It screens at most 100 addresses in one request. A payload that decodes to more is not screened at all, rather than screened in part.
  • It is rate limited per credential and environment.
A 429, a timeout, an undecodable payload, more than 100 addresses, or any other error on screenTransaction means the transaction was not screened. Dynamic’s SDKs fail open and proceed, and only an explicit block decision throws. Treat a failed screen as unscreened rather than as allowed.
Neither endpoint returns the match reason. Both accept an end-user JWT, so the response body is readable by the person being screened. The reason reaches you on the screening webhook events instead.
Last modified on October 6, 2026