Skip to content
decosa
LiveHostedSelf-hostSelf-host first for real data

Disposition a sanctions alert

A field-by-field comparison of the customer and the list entry, a proposed call with its rule, and a signed decision record naming the list version.

Held-out test0 of 1,094Same person wrongly proposed as a false positive (held-out test)
On production8.4 smedian on production (2026-09-26); slower when the service is busy
List price~$0.060 per 100 alertsmeasured, at list price

Built on: Typed judgment, Signed record

Loading the tool…

Use it your way

Use it from your codeThe hosted API with your key, and prompts to paste into a coding agent
Self-host · your GPUs · recommended

Run it yourself, on request

  • The same open models and app, on 1× RTX PRO 6000 (96 GB) or 1× RTX 5090 (32 GB) for Qwen3.8-27B; the comparison, the proposal and the record run on CPU.
  • Data never leaves your machines, and there are no Decosa charges.
  • One prompt for Claude Code or Codex assembles the whole stack.
  • Early access: the container images are not public yet and the source needs access; the prompt says how to ask.
Hosted · by Decosa

Get an API key

  • Call the sanctions alert disposition record API from your own code in minutes.
  • Every model answer carries a signed receipt.
  • Synthetic, public or test data only: real confidential data belongs on your own hardware.

Build with it

Paste one of these into Claude Code, Codex or another coding agent. The first wires your project to the hosted API with your DECOSA_API_KEY. The second pulls our containers and runs the same stack on your own GPU, with no Decosa charges.

Base URL
https://api.decosa.ai
Auth
Authorization: Bearer $DECOSA_API_KEY (or a demo session token)
Tool id
sanctions-disposition-record

Use the hosted API

# Decosa sanctions alert disposition record: use the hosted API (synthetic customers only)

You are wiring Decosa's sanctions alert disposition record into this project. For one screening alert (a customer or
payment party against one sanctions list entry) it compares every identifier in code (name with transliteration and
name order, date of birth, nationality, place of birth, ID numbers, IMO, address, kind of party) and proposes true match,
false positive or needs more information by a named rule that follows OFAC FAQ 5. An open model gives an independent
typed reading and writes a rationale whose every sentence is checked against the fields it cites. The analyst decides,
and the decision is sealed as a signed, hash-chained record that pins the list version. Use only what is listed below.
If you need something else, stop and ask me.

- Base URL: `https://api.decosa.ai`
- Health check: `GET https://api.decosa.ai/healthz`.
- **The hosted API takes synthetic customer records only.** Every review must carry `"synthetic": true` (HTTP 422
  otherwise). List entries are real public data (OFAC SDN and Consolidated, EU, UK). Never send a real customer here;
  real alerts belong on a self-hosted box (see the self-host prompt).
- It is decision support for an analyst, not a screening engine and never a compliance determination. Say so wherever
  you show results. The analyst decides.

## Auth: API key (or a demo session)
1. Preferred: an API key (`dk_…`) from "Get an API key" on the tool page, kept in `DECOSA_API_KEY`, never in code.
   Send `Authorization: Bearer $DECOSA_API_KEY`.
2. Without a key: `POST https://api.decosa.ai/demo/session` with `{"vertical": "sanctions-disposition-record"}` returns
   `{"token", "expires_at", "budget"}`. Sessions per IP are limited (HTTP 429 with `Retry-After`).
3. A review needs about 410 generated tokens of budget (402 otherwise). One run at a time per demo token (409).

## Endpoints
- `GET /sanctions/samples`, `GET /sanctions/info`, `GET /sanctions/lists` (no token): samples, the rules and policy,
  the regulatory notes, and the loaded list snapshot (version, source files, sha256, publish dates).
- `GET /sanctions/search?q=<name>` and `GET /sanctions/entry/<uid>` (no token): find the list entry an alert hit.
  A lookup, not screening. Uids look like `ofac-sdn:11358`, `ofac-cons:9640`, `eu:13`, `uk:AFG0006`.
- `POST /sanctions/review` (token). Body:
  ```json
  {"customer": {"ref": "CUST-1", "type": "individual", "name": "Mohammed Farouk", "dob": "1949-03-12",
                "nationality": "Pakistan", "ids": [{"type": "passport", "number": "X123", "country": "PK"}],
                "address": {"city": "...", "country": "..."}},
   "entry_uid": "ofac-sdn:11358", "alert": {"id": "ALR-1", "transaction_date": "2026-09-25"},
   "policy": {"fp_min_disqualifiers": 1}, "synthetic": true, "stream": false}
  ```
  Answer: `{analysis: {fields: [{id, label, status, strength, customer, listed, detail}], proposal: {decision, rule,
  because, why, ask_for}}, judgment: {answer, p_bp, agrees_with_proposal, reason}, sentences: [{text, cites, status:
  grounded | held, reasons}], receipts, review}`. Show held sentences as held; never present the model's reading as the
  decision. SSE with `Accept: text/event-stream`: `entry`, `analysis`, `receipt`, `judgment`, `sentence`, `review`.
- `POST /sanctions/decide` (token, no model call): `{review, decision: true_match | false_positive | needs_more_info,
  analyst: {id}, note?, override_reason? (20+ characters when you differ from the proposal), second_reviewer? (needed
  to clear against a true-match proposal or a matching identifier), prev_record_sha256?}` returns `{record,
  record_sha256, markdown, retain_until}`. Store the record yourself for 10 years; pass its `record_sha256` as
  `prev_record_sha256` on the next decision so your log forms one chain.
- `POST /sanctions/verify` (no token): `{record}`, `{records: [...]}` (checks the chain links) or `{manifest, csv}`.
- `POST /sanctions/audit-sample` (token): `{records, size?, seed?}` returns a signed, reproducible sample and a CSV.

## Errors
400 bad input (the message names the field), 401/403 token, 402 budget, 404 unknown list entry, 409/429 busy
(`Retry-After`), 413 body too large, 422 not marked synthetic or a decision the rules refuse (the message says why).

Run it yourself (containers)

On request. The container images and the compose file aren’t public yet. Ask for self-host access and Decosa sends the registry (DECOSA_REGISTRY) and the compose file’s URL (DECOSA_COMPOSE_URL) these steps use. They are the steps we tested end to end on a fresh machine.

# Decosa sanctions alert disposition record: run it yourself (containers)

You are setting up the Decosa sanctions alert disposition record on this machine, so customer records never leave it.
For one screening alert it compares the customer with the list entry field by field in code, proposes a disposition by
a named rule, has an open model explain it (every sentence checked against the fields it cites), and seals the
analyst's decision as a signed, hash-chained record that pins the list version. Nothing is sent to Decosa's hosted API.
It is decision support, not a screening engine: the analyst decides.

Status: the container images (${DECOSA_REGISTRY}/decosa-*) and the compose file are on request while self-host is in early access (not on a public registry yet): ask at https://decosa.ai/contact?topic=self-host, and Decosa sends the registry as DECOSA_REGISTRY, the compose file URL as DECOSA_COMPOSE_URL, and pull access. If a pull fails with
"not found", "denied" or "unauthorized", stop and tell me. Do not substitute other images.

Ask me before any command that needs sudo, and show me the command first.

## Step 0: set up with a coding agent, rehearse on mock data, then go private

This prompt is for a coding agent running on the machine that will host the service. We recommend Claude Code with
Claude Opus 5.5; any capable coding agent works. Work in this order:

1. Set up on mock data only. During the whole setup you (the agent) work with the synthetic sample bundle below and
   nothing else. Do not ask me for real data, and do not open, read, list or copy files that hold real data, even to
   "test with something realistic".
2. Rehearse. When the steps below are done and the service is healthy, fetch the mock-data bundle for this tool,
   https://decosa.ai/samples/sanctions-disposition-record.zip (2 KB, 13 checks, synthetic or openly licensed: see `licence` in expected.json),
   show me what is in it, and run the rehearsal against the local API:
   `docker compose exec api python scripts/rehearse.py sanctions-disposition-record` (the api image carries the same bundle under /app/rehearsal/sanctions-disposition-record/;
   with no key set, the script asks the local API for a short demo token). From a decosa-api checkout instead:
   `python scripts/rehearse.py sanctions-disposition-record --bundle sanctions-disposition-record.zip --base-url http://127.0.0.1:<PORT>`.
   It sends the mock inputs to the local API and prints PASS or FAIL for each expected property (for example: "the code proposes a false positive", "by the disqualifier rule", "the date of birth is the disqualifier"). Show me
   the full output. Every check must pass. If one fails, fix the install and run it again; never edit `expected.json`
   to make a check pass.
3. Stop there. Once the rehearsal passes, tell me, and I will run my own data against the local API myself, on this
   machine.

For the person running this: a coding agent that runs in the cloud sees everything in its context, including files it
reads, command output and anything pasted into the chat. Keep real data out of the chat and out of anything the agent
can read. Switch to your own data only after the rehearsal has passed and the agent's work is done.

## Steps
1. Docker: if `docker compose version` fails, install Docker Engine and the compose plugin using Docker's official
   instructions for this distribution (docs.docker.com/engine/install). Install the NVIDIA container toolkit and check
   `docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi`.
2. Fetch the compose file:
   `mkdir -p ~/decosa && cd ~/decosa && curl -fsSL "${DECOSA_COMPOSE_URL}" -o compose.yaml`
   Read it. Keep the `llm` service (Qwen3.8-27B on vLLM) and the `api` service. For the `api` service set
   `DECOSA_LLM_ROUTE=direct`, `DECOSA_LLM_URL=http://llm:8000/v1`, `DECOSA_LLM_MODEL=qwen3.8-27b`,
   `DECOSA_SANCTIONS_SYNTHETIC_ONLY=0` (so this box accepts real customers) and bind every port to 127.0.0.1. Never set
   the gateway route on this box: it would send customer data to the Decosa API.
3. Pull and start: `docker compose pull && docker compose up -d`. Wait for the `llm` health check (the first start
   downloads about 20 GB of weights).
4. Check: `curl -fsS http://127.0.0.1:<PORT>/sanctions/lists` shows the bundled list snapshot (its version and each
   source file's sha256 and publish date). To refresh the lists, run inside the api container
   `python -m decosa_api.verticals.sanctions.lists build --fetch /data/lists --out /data/snapshot.json.gz`, set
   `DECOSA_SANCTIONS_SNAPSHOT=/data/snapshot.json.gz` and restart the api. Keep every snapshot you used: a record names it.
   `GET /attest/signing-key` shows this box's public key; show it to me, it is what a reviewer pins.
5. Smoke test: get a token with `POST /demo/session {"vertical":"sanctions-disposition-record"}`, fetch
   `GET /sanctions/samples`, and send the `dob-mismatch` sample's `body` to `POST /sanctions/review`. Expect
   `analysis.proposal.decision: "false_positive"`, rule `R-DISQ`, F.dob `conflict` and F.nationality `match`, two
   receipts (attested by this box) and at least one grounded rationale sentence. Then `POST /sanctions/decide` with
   `{review, decision: "false_positive", analyst: {id: "setup-check"}}` and `POST /sanctions/verify` with the record:
   `ok` must be true.
6. Report back: the public key, the snapshot version, the proposal and how long the review took.

Off by default. Joining as a provider serves other people's requests on this GPU; never do it on a box that holds
customer data. If I ask for it later, follow the Provide page instead of improvising.
Run it on your own hardwareWhat it needs, and the prompt that sets it up

Run it on your own GPU

Same app, same pinned models, your hardware. Nothing goes to our servers and there are no Decosa charges.

  • CPU only, 64 GB RAMlite tierRuns with a smaller tier

    The standard tier does not fit: Qwen3.8-27B (NVFP4) needs a GPU. The lite tier fits.

  • GeForce RTX 4090standard tierRuns

    The standard tier fits with changes: Replace Qwen3.8-27B (NVFP4) with A community 4-bit build of Qwen3.8-27B (AWQ or GGUF). This build is NVIDIA NVFP4, which needs a Blackwell GPU. (Memory is an estimate.)

  • GeForce RTX 5090standard tierRuns

    The standard tier fits with changes: Qwen3.8-27B (NVFP4): run it at its smallest setting (about 28 GB instead of 57.6 GB), with a shorter context and fewer parallel sessions.

  • 2x GeForce RTX 5090standard tierRuns

    The standard tier fits with changes: Split the language model across the GPUs with tensor parallelism (vLLM --tensor-parallel-size).

  • L40Sstandard tierRuns

    The standard tier fits with changes: Replace Qwen3.8-27B (NVFP4) with Qwen3.8-27B official FP8. This build is NVIDIA NVFP4, which needs a Blackwell GPU.

  • H100 80 GB (SXM)standard tierRuns

    The standard tier fits with changes: Replace Qwen3.8-27B (NVFP4) with Qwen3.8-27B official FP8. This build is NVIDIA NVFP4, which needs a Blackwell GPU.

  • RTX PRO 6000 Blackwell 96 GBstandard tierRuns

    The standard tier fits (57.6 of 96 GB).

  • 2x RTX PRO 6000 Blackwell 96 GBstandard tierRuns

    The standard tier fits (57.6 of 192 GB).

  • Apple M3 Ultra (Mac Studio), 96 GBstandard tierRuns

    The standard tier fits with changes: Replace Qwen3.8-27B (NVFP4) with Qwen3.8-27B MLX 4-bit. MLX build for Apple Silicon.

  • Apple M5 Max, 64 GBstandard tierRuns

    The standard tier fits with changes: Replace Qwen3.8-27B (NVFP4) with Qwen3.8-27B MLX 4-bit. MLX build for Apple Silicon.

Memory per component comes from measured footprints, the tool's stack.json, or an estimate from its parameter count, and each is labelled that way below. Only an RTX PRO 6000 and an M3 Ultra Mac Studio have actually been run.

On request. The container images and the compose file aren’t public yet. Ask for self-host access and Decosa sends the registry (DECOSA_REGISTRY) and the compose file’s URL (DECOSA_COMPOSE_URL) these steps use. They are the steps we tested end to end on a fresh machine.

  1. 1

    Check the GPU, Docker and the NVIDIA Container Toolkit

    The driver must see the GPU, and Docker must be able to pass it into a container.

    nvidia-smi
    docker compose version
    docker run --rm --gpus all ubuntu nvidia-smi
  2. 2

    Fetch the compose file

    One file describes the API and the language model as services.

    mkdir -p ~/decosa && cd ~/decosa
    curl -fsSL "${DECOSA_COMPOSE_URL}" -o compose.yaml
  3. 3

    Pull and start

    The first start downloads pinned model weights, tens of gigabytes.

    docker compose pull
    docker compose up -d
  4. 4

    Check health

    Wait until the API reports ok with the language model loaded. Then point your app at the local base URL.

    curl -fsS http://localhost:<PORT>/healthz
    # {"ok": true, "llm": true, ...}
    curl -fsS -X POST http://localhost:<PORT>/demo/session \
      -H 'Content-Type: application/json' -d '{"vertical":"sanctions-disposition-record"}'

Set up with a coding agent, rehearse on mock data, then go private

  1. Set up with a coding agent. Paste the self-host prompt into a coding agent on the machine that will run the service. We recommend Claude Code with Claude Opus 5.5; any capable coding agent works.
  2. Rehearse on mock data. The agent runs the tool on a bundle of synthetic inputs and checks each answer against the bundle's expected.json. Every check must print PASS.
  3. Go private. Only then do you run your own data against the local API, yourself, on that machine. Never give the agent real data during setup: a coding agent that runs in the cloud sees everything in its context, so keep real data out of the chat and out of the files it reads.
Rehearsal command
docker compose exec api python scripts/rehearse.py sanctions-disposition-record

Download the mock-data bundle (2 KB, 13 checks)expected.json

A fictional customer, Ali Zeaiter (born 2 Nov 1977, Lebanese), hits the real OFAC SDN entry ofac-sdn:17037 (Ali ZEAITER, born 24 Feb 1977, Lebanon). The code compares every field and proposes a false positive by rule R-DISQ: same year, different date of birth, and nothing else that identifies the person agrees. The model gives an independent reading and a rationale whose sentences are checked against the fields they cite. The analyst clears the alert; the decision is sealed in a signed, hash-chained record that verifies, and an edited record does not. A decision against the proposal without a reason is refused. Needs the list snapshot of 26 Sep 2026 (or any snapshot that still has ofac-sdn:17037).

What the rehearsal checks
  • the code proposes a false positive
  • by the disqualifier rule
  • the date of birth is the disqualifier
  • the nationality agrees
  • at least one rationale sentence is grounded in the fields it cites
  • the review is signed
  • deciding against the proposal without a reason is refused
  • the disposition record verifies
  • the record keeps the analyst's decision
  • the record is kept for 10 years
  • a record with its decision changed no longer verifies
  • the audit sample takes the record and is signed
  • every model call has a signed receipt

Licence: The customer is fictional (made up for this bundle). The list entry is public OFAC SDN data (US government work, public domain). Part of decosa-api, AGPL-3.0-or-later.

Prompt for your coding agent

# Decosa sanctions alert disposition record: run it yourself (containers)

You are setting up the Decosa sanctions alert disposition record on this machine, so customer records never leave it.
For one screening alert it compares the customer with the list entry field by field in code, proposes a disposition by
a named rule, has an open model explain it (every sentence checked against the fields it cites), and seals the
analyst's decision as a signed, hash-chained record that pins the list version. Nothing is sent to Decosa's hosted API.
It is decision support, not a screening engine: the analyst decides.

Status: the container images (${DECOSA_REGISTRY}/decosa-*) and the compose file are on request while self-host is in early access (not on a public registry yet): ask at https://decosa.ai/contact?topic=self-host, and Decosa sends the registry as DECOSA_REGISTRY, the compose file URL as DECOSA_COMPOSE_URL, and pull access. If a pull fails with
"not found", "denied" or "unauthorized", stop and tell me. Do not substitute other images.

Ask me before any command that needs sudo, and show me the command first.

## Step 0: set up with a coding agent, rehearse on mock data, then go private

This prompt is for a coding agent running on the machine that will host the service. We recommend Claude Code with
Claude Opus 5.5; any capable coding agent works. Work in this order:

1. Set up on mock data only. During the whole setup you (the agent) work with the synthetic sample bundle below and
   nothing else. Do not ask me for real data, and do not open, read, list or copy files that hold real data, even to
   "test with something realistic".
2. Rehearse. When the steps below are done and the service is healthy, fetch the mock-data bundle for this tool,
   https://decosa.ai/samples/sanctions-disposition-record.zip (2 KB, 13 checks, synthetic or openly licensed: see `licence` in expected.json),
   show me what is in it, and run the rehearsal against the local API:
   `docker compose exec api python scripts/rehearse.py sanctions-disposition-record` (the api image carries the same bundle under /app/rehearsal/sanctions-disposition-record/;
   with no key set, the script asks the local API for a short demo token). From a decosa-api checkout instead:
   `python scripts/rehearse.py sanctions-disposition-record --bundle sanctions-disposition-record.zip --base-url http://127.0.0.1:<PORT>`.
   It sends the mock inputs to the local API and prints PASS or FAIL for each expected property (for example: "the code proposes a false positive", "by the disqualifier rule", "the date of birth is the disqualifier"). Show me
   the full output. Every check must pass. If one fails, fix the install and run it again; never edit `expected.json`
   to make a check pass.
3. Stop there. Once the rehearsal passes, tell me, and I will run my own data against the local API myself, on this
   machine.

For the person running this: a coding agent that runs in the cloud sees everything in its context, including files it
reads, command output and anything pasted into the chat. Keep real data out of the chat and out of anything the agent
can read. Switch to your own data only after the rehearsal has passed and the agent's work is done.

## Steps
1. Docker: if `docker compose version` fails, install Docker Engine and the compose plugin using Docker's official
   instructions for this distribution (docs.docker.com/engine/install). Install the NVIDIA container toolkit and check
   `docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi`.
2. Fetch the compose file:
   `mkdir -p ~/decosa && cd ~/decosa && curl -fsSL "${DECOSA_COMPOSE_URL}" -o compose.yaml`
   Read it. Keep the `llm` service (Qwen3.8-27B on vLLM) and the `api` service. For the `api` service set
   `DECOSA_LLM_ROUTE=direct`, `DECOSA_LLM_URL=http://llm:8000/v1`, `DECOSA_LLM_MODEL=qwen3.8-27b`,
   `DECOSA_SANCTIONS_SYNTHETIC_ONLY=0` (so this box accepts real customers) and bind every port to 127.0.0.1. Never set
   the gateway route on this box: it would send customer data to the Decosa API.
3. Pull and start: `docker compose pull && docker compose up -d`. Wait for the `llm` health check (the first start
   downloads about 20 GB of weights).
4. Check: `curl -fsS http://127.0.0.1:<PORT>/sanctions/lists` shows the bundled list snapshot (its version and each
   source file's sha256 and publish date). To refresh the lists, run inside the api container
   `python -m decosa_api.verticals.sanctions.lists build --fetch /data/lists --out /data/snapshot.json.gz`, set
   `DECOSA_SANCTIONS_SNAPSHOT=/data/snapshot.json.gz` and restart the api. Keep every snapshot you used: a record names it.
   `GET /attest/signing-key` shows this box's public key; show it to me, it is what a reviewer pins.
5. Smoke test: get a token with `POST /demo/session {"vertical":"sanctions-disposition-record"}`, fetch
   `GET /sanctions/samples`, and send the `dob-mismatch` sample's `body` to `POST /sanctions/review`. Expect
   `analysis.proposal.decision: "false_positive"`, rule `R-DISQ`, F.dob `conflict` and F.nationality `match`, two
   receipts (attested by this box) and at least one grounded rationale sentence. Then `POST /sanctions/decide` with
   `{review, decision: "false_positive", analyst: {id: "setup-check"}}` and `POST /sanctions/verify` with the record:
   `ok` must be true.
6. Report back: the public key, the snapshot version, the proposal and how long the review took.

Off by default. Joining as a provider serves other people's requests on this GPU; never do it on a box that holds
customer data. If I ask for it later, follow the Provide page instead of improvising.

Help me customise for my hardware

Pick your GPU or Mac, or enter its memory. You get the tier that fits, the model swaps it needs, measured speed where we have it, and a setup prompt with those choices written in.

Hardware

GeForce RTX 5090: 32 GB GDDR7, 1,792 GB/s, FP8 and NVFP4. NVIDIA product page

RunsSanctions alert disposition record on GeForce RTX 5090: use the Standard · one GPU for the model (hosted demo) tier

The standard tier fits with changes: Qwen3.8-27B (NVFP4): run it at its smallest setting (about 28 GB instead of 57.6 GB), with a shorter context and fewer parallel sessions.

What this tool's stack says about this hardware:

  • 1x RTX 5090 32 GB (fits): Estimate: Qwen3.8-27B NVFP4 needs about 20 GB of weights plus KV cache; not run for this use case.

Standard · one GPU for the model (hosted demo): what changesuses estimates

  • Qwen3.8-27B (NVFP4): run it at its smallest setting (about 28 GB instead of 57.6 GB), with a shorter context and fewer parallel sessions.
Memory per component
  • List snapshot, field comparison, proposal rul...: decosa-api sanctions desk (decosa_api/verticals/sanctions), with the typed-judgment core (vertical 24) and the session hash chain (record, vertical 07). CPU. Runs on CPU (vram_gb 0 in stack.json).
  • Independent typed reading and the rationale: Qwen3.8-27B (NVFP4). ~57.6 GB (at least ~28 GB), weights 21.4 GB (from stack.json). Qwen3.8-27B NVFP4: Weights 19.9 GiB (21.4 GB), measured (field stack.json). The compose file gives the server 0.60 of a 96 GB card (57.6 GB) so the rest is FP8 KV cache for several sessions. The 28 GB minimum is an estimate: weights plus a short-context KV cache, which is why several stacks list a 32 GB RTX 5090 as 'estimate'. (stack.json lists 20 GB for this component.)

Expected speed

Not measured.

Not measured on this hardware. The only measured setups are an RTX PRO 6000 Blackwell and a Mac Studio M3 Ultra.

Setup prompt for this hardware

The self-host prompt for Sanctions alert disposition record, with a hardware plan for GeForce RTX 5090 added after Step 0. Loading the full prompt; until then it points your agent at the prompt's URL.

# Set up Sanctions alert disposition record on my hardware

Fetch https://decosa.ai/prompts/sanctions-disposition-record-selfhost.md and follow it (including Step 0: rehearse on mock data first), with the hardware plan below applied.

## Hardware plan for this machine (from https://decosa.ai/self-host/hardware?use=sanctions-disposition-record)

Target machine: GeForce RTX 5090 (32 GB of GPU memory; CUDA, FP8 and NVFP4).
Quality tier: Standard · one GPU for the model (hosted demo) (standard). Fit check: runs with changes, about 28 GB of 32 GB used; some memory numbers are estimates, not measurements.

First, check the machine: run `nvidia-smi` (or `rocm-smi`, or `sysctl hw.memsize` on a Mac) and confirm the GPUs and free memory match the line above. If they do not, stop and tell me before pulling anything.

Use these components (the setup below describes the standard tier; change it to match):
- List snapshot, field comparison, proposal rul...: decosa-api sanctions desk (decosa_api/verticals/sanctions), with the typed-judgment core (vertical 24) and the session hash chain (record, vertical 07), CPU
- Independent typed reading and the rationale: Qwen3.8-27B (NVFP4) (nvidia/Qwen3.8-27B-NVFP4), 57.6 GB. Change: Qwen3.8-27B (NVFP4): run it at its smallest setting (about 28 GB instead of 57.6 GB), with a shorter context and fewer parallel sessions.

GPU placement (set each service's device and its vLLM --gpu-memory-utilization to about the share shown):
- GPU 0: Qwen3.8-27B (NVFP4) ~28 GB (88%); about 4 GB left

During the rehearsal, watch GPU memory. If a model fails to load or runs out of memory, lower its --max-model-len and --max-num-seqs first, then its memory share, and tell me what you changed.

The stack's own component list and compose layout: https://decosa.ai/prompts/sanctions-disposition-record-assemble.md

The proof

How we tested itEval results and end-to-end checks, hosted and self-hosted, with dates

Verified end to end

Hosted: verified 26 Sep 2026 · measured 26 Sep 2026: · p50 8.4 s · ~<$0.001 per run · 2 receipts

Loading the nightly status…

Self-host: verified 26 Sep 2026 · fresh clone, compose up, sample against local model servers

Measured cost to run: about $0.060 per 100 alerts (hosted, 26 Sep 2026). Self-hosting is free: the code is open and the models are open-weight. You pay only for your own hardware and power.

A fresh clone of a decosa-api pre-release build (not yet merged to main), the api image built from docker/api/Dockerfile with DECOSA_SANCTIONS_SYNTHETIC_ONLY=0, run against the already-running local Qwen3.8-27B vLLM on the direct route. The dob-mismatch review took 0.86 s (false_positive by R-DISQ, both receipts attested, 3 grounded sentences); the record verified and an edited one failed; an override without a reason and a risky clear without a second reviewer both got 422. The documented list refresh ran in the container in 41 s. Model-server startup itself not re-verified.

Known limits (5)
  • Synthetic customers only on the hosted demo, and every number here comes from our own generator on real list entries; not yet run on a real alert queue.
  • A true match whose customer data is wrong (a mistyped date of birth or ID) can be proposed as a false positive; the analyst checks the source document.
  • Names in scripts other than Latin and Cyrillic are not compared, so those alerts are held.
  • It reviews one alert against one list entry. It does not screen, and it does not cover ownership (the 50 Percent Rule), licences or list changes after the snapshot.
  • Records are not stored: you keep them for 10 years.

Eval results, nightly checks and cost per runVerify a run

How it's builtThe steps, the models and what each one checks
Self-host · your GPUs · recommended

Run it yourself, on request

  • The same open models and app, on 1× RTX PRO 6000 (96 GB) or 1× RTX 5090 (32 GB) for Qwen3.8-27B; the comparison, the proposal and the record run on CPU.
  • Data never leaves your machines, and there are no Decosa charges.
  • One prompt for Claude Code or Codex assembles the whole stack.
  • Early access: the container images are not public yet and the source needs access; the prompt says how to ask.
Hosted · by Decosa

Get an API key

  • Call the sanctions alert disposition record API from your own code in minutes.
  • Every model answer carries a signed receipt.
  • Synthetic, public or test data only: real confidential data belongs on your own hardware.
The open stack

Clear a screening alert field by field, with a rule-based proposal, a rationale checked against the fields, and a signed record that pins the list version.

For compliance analysts clearing sanctions name-screening alerts. Give it one alert: the screened party and the OFAC, EU or UK list entry it hit. Code compares every identifier: the name (transliteration and name order handled), date of birth, nationality, place of birth, ID numbers, IMO number, address and the kind of party. It then proposes true match, false positive or needs more information by a named rule that follows OFAC's FAQ 5. It only proposes a false positive on a strong disqualifier. Qwen3.8-27B gives an independent typed reading and writes a short rationale. Every sentence of the rationale is checked in code against the fields it cites; the model explains, it never decides. The analyst decides. An override needs a reason, and clearing against a matching identifier needs a second reviewer. The decision is sealed in a signed, hash-chained record that holds the list snapshot's hash, the fields compared, the rationale, the analyst and the time. It links to the previous record, for a 10-year log you keep. It also draws a reproducible, signed audit sample. It is not a screening engine and never a compliance determination.

Deployment
Self-host first
Regulatory
Checked 26 Sep 2026 against primary sources (links under Tools). OFAC's recordkeeping rule, 31 CFR 501.601, now requires a full and accurate record of each transaction subject to OFAC's regulations, kept at least 10 years after the transaction; blocked-property records must be kept 10 years after unblocking. The change from 5 to 10 years came in an interim final rule (89 FR 74832, 13 Sep 2024, effective 12 Mar 2025), which a final rule adopted without change (90 FR 13286, published and effective 21 Mar 2025). It matches the 10-year statute of limitations for IEEPA and TWEA violations that took effect on 24 Apr 2024 (Pub. L. 118-50, as summarised in those rules). The rule text speaks of transactions and blocked property; it does not name screening alerts. Keeping an alert's disposition with the transaction record for the same 10 years is our conservative reading, not quoted text, so the record's retain_until is 10 years from the later of the decision and the transaction date. The proposal rules follow OFAC FAQ 5 (updated 9 Sep 2026). Step 3 there says to compare all the details on the list entry, that an address alone may not be enough, and that many potential matches are false positives. Step 4 says multiple matching identifiers suggest a valid match, and the organisation decides under its own procedures. Weak aliases follow OFAC FAQs 122-123. For the UK, the OFSI Consolidated List closed on 28 Jan 2026, and the UK Sanctions List (FCDO) is now the only source; the snapshot uses it. OFAC notes that EU data-retention limits can conflict with the 10-year rule (90 FR 13286); where they do, that is a question for your counsel. This is decision support, not legal advice and never a compliance determination: the analyst decides. The hosted demo takes synthetic customers only. Model licence: Apache-2.0 (Qwen3.8-27B). List data: US government works, EU list (Commission Decision 2011/833/EU reuse), UK list (Open Government Licence assumed, not stated in the file).
Architecture
Text description

One screening alert: the screened party (the customer record) and the list entry it hit. The list entry comes from a versioned snapshot of the OFAC SDN and Consolidated lists, the EU consolidated list and the UK Sanctions List, with each source file hashed. The disposition desk, plain code on CPU, does four things: it compares every field (name with transliteration and order, date of birth, nationality, IDs, IMO, address, kind of party); it proposes a disposition by a named rule following OFAC FAQ 5; it checks each rationale sentence against its fields; and it seals the decision. Qwen3.8-27B, open weights under Apache-2.0, gives an independent typed reading and writes the rationale in two calls per alert; it explains and never decides. The analyst decides between true match, false positive and needs more information. An override needs a reason, and a risky clear needs a second reviewer. Outputs, which you keep: a signed disposition record (list version, every field, proposal, rationale, analyst, time, decision) chained to the previous record by hash and kept 10 years under 31 CFR 501.601; a Markdown memo; and a signed, reproducible audit sample. On the hosted route each model call gets a gateway-signed receipt. Self-hosted, receipts are signed by your own key and customer records never leave the machine.

Architecture

At a glance

Data retention
Nothing stored: the alert lives in memory for the request, and the review and the record go back to you. Logs carry the list, rule and decision, never names, dates of birth or numbers. You keep the records (10 years under 31 CFR 501.601).
What leaves the box
Hosted: the two model calls go through our gateway to the GPU serving Qwen3.8-27B, and only synthetic customers are accepted. Self-hosted on the direct route: nothing leaves the box. Refreshing the lists downloads the public list files; no customer data is sent.
What it will not do
Decide, screen your customer base, or say 'compliant'. It proposes; the analyst decides and the record says who.
Lists
OFAC SDN and Consolidated, EU consolidated, UK Sanctions List: 32,452 entries in the 26 Sep 2026 snapshot, each source file's sha256 and publish date recorded. Rebuild the snapshot any day with one command.
Typical run
Two model calls, a fraction of a cent at the gateway list price; the analysis and the record are free.
Quality tiers

Pick the tier for the quality you need

Same app at every tier. What changes is the models, the hardware they need, and whether receipts are signed. Scores are measured with the source named, or marked not measured.

  • In the hosted demo

    Lite

    the comparison and the proposal, no GPU

    POST /sanctions/analyze compares every field and proposes a disposition by rule, and /decide seals the analyst's decision; no model reading, no rationale.

    Models
    • decosa-api sanctions desk (decosa_api/verticals/sanctions), with the typed-judgment core (vertical 24) and the session hash chain (record, vertical 07)
    Hardware
    Any CPU (about 200 MB of RAM for the list snapshot)
    Quality evidence
    • Same-party pairs proposed as false positive (held-out test split)0 of 1,094docs/evals/sanctions-disposition-record.md, part A, 26 Sep 2026
    • False-positive clearance precision (test)635 of 635 (100%)docs/evals/sanctions-disposition-record.md, part A
    • Field status accuracy on labelled fields (test)99.77% of 2,199docs/evals/sanctions-disposition-record.md, part A
    Latency
    measured: under a millisecond per alert in process
    Verification
    No proof yetNo model call, so no receipts; the record is signed by the instance key.
  • In the hosted demo

    Standard

    one GPU for the model (hosted demo)

    Adds Qwen3.8-27B's independent typed reading and a rationale whose every sentence is checked against the fields it cites. This is what the hosted demo runs, on synthetic customers only.

    Models
    • decosa-api sanctions desk (decosa_api/verticals/sanctions), with the typed-judgment core (vertical 24) and the session hash chain (record, vertical 07)
    • Qwen3.8-27B (NVFP4)
    Hardware
    1x RTX PRO 6000 96 GB (measured) or 1x RTX 5090 32 GB (estimate)
    Quality evidence
    • Typed reading agrees with the code proposal (81 test reviews)71 of 81 (88%); it never proposed clearing a same-party pairdocs/evals/sanctions-disposition-record.md, part B
    • Rationale sentences passing the code check278 of 278docs/evals/sanctions-disposition-record.md, part B
    • Planted rationale errors held by the checker (wrong year, country, ID, polarity, citation)2,776 of 2,776; 531 of 531 correct sentences passeddocs/evals/sanctions-disposition-record.md, part C
    • Invented facts in a manual read of 30 sampled rationale sentences0docs/evals/sanctions-disposition-record.md, part B
    Latency
    measured: seconds per review through the shared gateway; faster when it was quiet.
    Verification
    Proof: strongBoth model calls are separate gateway calls with gateway-signed receipts, embedded in the record.

Also runs on

  • Names in every scriptA licence-clean multilingual transliteration or name-matching model (not chosen)not builtCompare names written in Arabic, Persian, Chinese and Korean script directly, not only their Latin forms. The model is not chosen yet; it must be licence-clean. Hardware: 1x RTX PRO 6000 96 GB (estimate).

We host these ourselves when needed: small models get more of our own compute unless we detect a shortage, so they need no community providers.

Components

Every model in the stack

Models in this stack. Each row has a button that shows its licence, engine, verification and evidence.
ModelDetails
List snapshot, field comparison, proposal rules, rationale check, signed record, audit sample (no model; CPU)decosa-api sanctions desk (decosa_api/verticals/sanctions), with the typed-judgment core (vertical 24) and the session hash chain (record, vertical 07)
0 GBProof: partial
Independent typed reading and the rationaleQwen3.8-27B (NVFP4)nvidia/Qwen3.8-27B-NVFP4 on Hugging Face (opens in a new tab)
27.8B · 20 GBProof: strongIn the hosted demo
Transliteration for Arabic, Persian, Chinese and Korean names (alternate)A licence-clean multilingual transliteration or name-matching model (not chosen)
No proof yetSelf-host only

Around the models

Tools, services and hardware

Tools

Services

  • decosa-api:8445
    ${DECOSA_REGISTRY}/decosa-api:<tag>

    GET /sanctions/info, /samples, /lists, /entry, /search; POST /sanctions/analyze, /review (SSE or JSON), /decide, /verify, /audit-sample. The list snapshot ships inside the image. Keeps no customer data.

  • vLLM (model):8114
    vllm/vllm-openai@sha256:c2914767605584b6d8f45686b82de173ecc99e781897aa3d0a66dacd72c51ae1

    Qwen3.8-27B NVFP4 behind our gateway (hosted) or called directly (self-host).

Hardware

  • 1x RTX PRO 6000 Blackwell 96 GB Fits

    Measured on our server: the hosted demo and the eval ran on this card through the shared gateway.

  • 1x RTX 5090 32 GB Fits

    Estimate: Qwen3.8-27B NVFP4 needs about 20 GB of weights plus KV cache; not run for this tool.

  • CPU only Fits

    The lite tier (POST /sanctions/analyze: the comparison and the proposal) and the record need no GPU. The loaded snapshot takes about 200 MB of RAM.

Latency per lane

  • review: code analysis plus both model calls in parallel, hosted gateway route8.4 s

    Measuredmeasured on our server 2026-09-26: median of 81 eval reviews (p90 12.9 s, max 15.4 s) with the gateway shared with other workloads; 2.6 s on a quiet gateway

  • code analysis only (POST /sanctions/analyze)n/a

    Measuredmeasured on our server 2026-09-26: 0.4 ms per pair over 1,967 test pairs, in process (under 1 ms)

  • seal the decision (POST /sanctions/decide)n/a

    Measuredno model call; milliseconds on CPU (not separately timed)

Notes

  • The risky direction is clearing a true match. On 1,094 held-out same-party pairs (synthetic customers built from real list entries, with transliteration variants, held-out aliases, Cyrillic names, typos, renewed passports and partial data), the code proposed a false positive 0 times. All 635 of its false-positive proposals were different parties.
  • It proposed a true match on 79% of same-party pairs. The rest were held as needs more information because the data could not confirm them: name only, a day/month swap, a renewed passport. Of different parties, it cleared 73% and held the rest.
  • A true match whose customer record carries a wrong date of birth or ID would be proposed as a false positive: the tool cannot tell a typo from a different person, except for a swapped day and month or a one-year slip. Check the source document.
  • The model's independent reading agreed with the code on 71 of 81 reviews and never proposed clearing a same-party pair. All 278 rationale sentences passed the code check. A manual read of 30 found no invented facts.
  • Names in Arabic, Persian, Chinese, Korean and other scripts besides Latin and Cyrillic are not transliterated: such a name is 'not compared' and the alert is held.
  • Everything here is measured on synthetic customers written by the agent that built the rules; it has not been run on a real alert queue.
Assemble it

Run this exact stack on your machine

Paste into Claude Code / Codex to assemble this stack locally. The prompt checks your GPU, pulls the pinned models, writes the compose file and runs a smoke test.

sanctions-disposition-record/assemble-prompt.md159 lines
# Assemble the Decosa sanctions alert disposition record on this machine

You are setting up decision support for compliance analysts who clear sanctions name-screening alerts. For one alert
(a customer or payment party against one sanctions list entry) it does four things:
- It compares every identifier in code: name with transliteration and name order, date of birth, nationality, place of
  birth, ID numbers, IMO, address, and the kind of party.
- It proposes true match, false positive or needs more information, by a named rule that follows OFAC FAQ 5.
- An open model gives an independent typed reading and writes a rationale. Code checks every sentence against the
  fields it cites.
- The analyst's decision is sealed in a record signed by this box's own key. The record pins the list version and links
  to the previous record.

Work step by step, show me each command before you run anything with `sudo`, and stop to ask if a check fails.

## Step 0: set up with a coding agent, rehearse on mock data, then go private

This prompt is for a coding agent running on the machine that will host the service. We recommend Claude Code with
Claude Opus 5.5; any capable coding agent works. Work in this order:

1. Set up on mock data only. During the whole setup you (the agent) work with the synthetic sample bundle below and
   nothing else. Do not ask me for real data, and do not open, read, list or copy files that hold real data, even to
   "test with something realistic".
2. Rehearse. When the steps below are done and the service is healthy, fetch the mock-data bundle for this tool,
   https://decosa.ai/samples/sanctions-disposition-record.zip (2 KB, 13 checks, synthetic or openly licensed: see `licence` in expected.json),
   show me what is in it, and run the rehearsal against the local API:
   `docker compose exec api python scripts/rehearse.py sanctions-disposition-record` (the api image carries the same bundle under /app/rehearsal/sanctions-disposition-record/;
   with no key set, the script asks the local API for a short demo token). From a decosa-api checkout instead:
   `python scripts/rehearse.py sanctions-disposition-record --bundle sanctions-disposition-record.zip --base-url http://127.0.0.1:<PORT>`.
   It sends the mock inputs to the local API and prints PASS or FAIL for each expected property (for example: "the code proposes a false positive", "by the disqualifier rule", "the date of birth is the disqualifier"). Show me
   the full output. Every check must pass. If one fails, fix the install and run it again; never edit `expected.json`
   to make a check pass.
3. Stop there. Once the rehearsal passes, tell me, and I will run my own data against the local API myself, on this
   machine.

For the person running this: a coding agent that runs in the cloud sees everything in its context, including files it
reads, command output and anything pasted into the chat. Keep real data out of the chat and out of anything the agent
can read. Switch to your own data only after the rehearsal has passed and the agent's work is done.

## 0. Ground rules and licences
- Model: Qwen3.8-27B (Apache-2.0), two short calls per alert: the typed second reading and the rationale. The
  comparison, the proposal, the rationale check and the record are decosa-api (AGPL-3.0-or-later) and run on CPU.
- Lists: the bundled snapshot comes from the public OFAC SDN and Consolidated lists (US government work), the EU
  consolidated list (reuse allowed with acknowledgement) and the UK Sanctions List (Crown copyright, Open Government
  Licence assumed). Keep every snapshot you use; each record names its snapshot by hash.
- Customer records stay on this machine. Bind every port to 127.0.0.1. The service stores nothing: the review and the
  record go back to the caller, and logs carry decisions and counts, never names, dates of birth or numbers.
- Be honest about what it is. It is not a screening engine, and it never makes a compliance determination. The analyst
  decides.

## 1. Check the machine
1. `nvidia-smi`: one GPU with at least 32 GB. Qwen3.8-27B NVFP4 needs about 20 GB of weights plus KV cache. We measured
   on an RTX PRO 6000 96 GB; an RTX 5090 32 GB should fit, but we have not run it. Driver 570 or newer. Blackwell cards
   run NVFP4; on older cards use `Qwen/Qwen3.8-27B-FP8`. Without a GPU, `POST /sanctions/analyze` (the code analysis and
   proposal) still works.
2. `docker --version` and `docker compose version`. If Docker or the NVIDIA container toolkit is missing, ask me first,
   then install them from the official Docker and NVIDIA repositories. Check with
   `docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi`.
3. Disk: about 30 GB free.

## 2. Images and weights
- API image: `${DECOSA_REGISTRY}/decosa-api:<tag>` (**publishing soon**). If the pull fails, build from source:
  `git clone <decosa-api source: on request at https://decosa.ai/contact?topic=self-host>` (access required). Check out the newest release tag that
  contains `decosa_api/verticals/sanctions/` (use `main` until one does), then build `docker/api/Dockerfile`. The list
  snapshot (`decosa_api/verticals/sanctions/data/snapshot.json.gz`, 3.4 MB) is package data inside the image.
- Model: `vllm/vllm-openai:v0.29.0` with weights `nvidia/Qwen3.8-27B-NVFP4` (revision
  `482ca0f3832238542f8f5295dde86b5f22711d80`), or `Qwen/Qwen3.8-27B-FP8` on a card without NVFP4.

## 3. docker-compose.yml
Write this in `~/decosa/sanctions/`:

```yaml
services:
  llm:
    image: vllm/vllm-openai:v0.29.0
    command: ["--model", "nvidia/Qwen3.8-27B-NVFP4", "--served-model-name", "qwen3.8-27b", "--max-model-len", "32768",
              "--enable-prefix-caching"]
    ports: ["127.0.0.1:8114:8000"]
    volumes: ["~/.cache/huggingface:/root/.cache/huggingface"]
    deploy: { resources: { reservations: { devices: [{ driver: nvidia, count: 1, capabilities: [gpu] }] } } }
    healthcheck: { test: ["CMD", "curl", "-fs", "http://localhost:8000/v1/models"], interval: 30s, retries: 20 }
  api:
    image: ${DECOSA_REGISTRY}/decosa-api:<tag>
    ports: ["127.0.0.1:8445:8445"]
    environment:
      DECOSA_HOST: 0.0.0.0
      DECOSA_PORT: "8445"
      DECOSA_DATA_DIR: /data
      DECOSA_LLM_ROUTE: direct
      DECOSA_LLM_URL: http://llm:8000/v1
      DECOSA_LLM_MODEL: qwen3.8-27b
      DECOSA_SANCTIONS_SYNTHETIC_ONLY: "0"
      DECOSA_SANCTIONS_MAX_CONCURRENT: "4"
      DECOSA_BUDGET_LLM_TOKENS: "60000"
    volumes: ["decosa-data:/data"]
    depends_on: { llm: { condition: service_healthy } }
    healthcheck: { test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8445/sanctions/lists', timeout=4)"], interval: 30s, retries: 10 }
volumes:
  decosa-data:
```

About these settings:
- `DECOSA_SANCTIONS_SYNTHETIC_ONLY: "0"` lets this box take real customer records. The hosted service refuses anything
  not marked `"synthetic": true`.
- The api keeps its state (keys, receipts, this box's signing key) in the named volume `decosa-data`, not in a host
  folder. The image runs as an unprivileged user (uid 10001). A host folder that Docker creates is owned by root, and
  the api then stops with `PermissionError`.

Start everything with `docker compose up -d`.

On the first start the api creates this box's Ed25519 key in the volume (`/data/attest/`, mode 0600). Back it up with
`docker compose cp api:/data/attest ./attest-backup`, keep that copy private, and never print it.

Every review, record and model call on the direct route is signed with that key (receipt status `attested`). That is
an attestation by me, the operator, not a proof of computation. Never set `DECOSA_LLM_ROUTE=gateway` on this box: that
sends customer data to the Decosa API.

## 4. Refresh the lists (daily, or when OFAC publishes)
```
docker compose exec api python -m decosa_api.verticals.sanctions.lists build --fetch /data/lists --out /data/snapshot-$(date +%F).json.gz
```
Then set `DECOSA_SANCTIONS_SNAPSHOT: /data/snapshot-<date>.json.gz` in the compose file and run
`docker compose up -d api`. The build prints the new version and each source file's sha256. Keep old snapshot files:
a record from last year names last year's version, and an examiner may ask for it. `GET /sanctions/lists` shows which
snapshot is loaded.

## 5. Smoke test
1. `curl -s localhost:8445/sanctions/lists | jq '{version, entries, sources: [.sources[] | {list, publish_date}]}'`
   should show four lists and about 32,000 entries (fewer or more after a refresh).
2. Get a token:
   `T=$(curl -s -XPOST localhost:8445/demo/session -H 'content-type: application/json' -d '{"vertical":"sanctions-disposition-record"}' | jq -r .token)`.
3. Take the sample and review it:
   - `curl -s localhost:8445/sanctions/samples | jq '.[] | select(.id=="dob-mismatch") | .body' > alert.json`
   - `curl -s -XPOST localhost:8445/sanctions/review -H "authorization: Bearer $T" -H 'content-type: application/json' -d @alert.json > review.json`

   Expect:
   - `.analysis.proposal` to show `false_positive` by rule `R-DISQ`, with F.dob `conflict` and F.nationality `match`;
   - two receipts with status `attested`;
   - at least one rationale sentence with status `grounded`.
4. Decide and verify:
   `jq '{review: .review, decision: "false_positive", analyst: {id: "setup-check"}}' review.json | curl -s -XPOST localhost:8445/sanctions/decide -H "authorization: Bearer $T" -H 'content-type: application/json' -d @- > decided.json`,
   then `jq '{record: .record}' decided.json | curl -s -XPOST localhost:8445/sanctions/verify -H 'content-type: application/json' -d @- | jq .ok`
   must print `true`. Change `.record.statement.decision` and verify again: it must print `false`.
5. Deciding against the proposal without `override_reason` must answer 422. So must clearing the `vessel-imo` sample
   (its IMO matches) without a `second_reviewer`.
6. Time the review and tell me what you measure. On our shared GPU through the gateway it took 3-15 s. The direct
   route on an idle card is usually faster.

## 6. Point your alert queue at it
- Set `NEXT_PUBLIC_DECOSA_API=http://127.0.0.1:8445` in the site's `.env.local`, or call it from your case manager:
  `POST /sanctions/review` per alert, show the fields, the proposal and the rationale to the analyst, then
  `POST /sanctions/decide` with their decision.
- Store each record for 10 years in your own storage. Pass the previous record's `record_sha256` as
  `prev_record_sha256` so the log forms one chain.
- For QA or an examiner: `POST /sanctions/verify {records: [...]}` checks the chain, and `POST /sanctions/audit-sample`
  draws a reproducible, signed sample.
- Contract: `API_CONTRACT.md`, section "Sanctions alert disposition record".

Off by default. Joining serves other people's requests on this GPU. Never do it on a box that holds customer data. If I
ask for it, follow the provider guide at `/provide` on the site, and do not turn it on without my explicit yes.
Rules and regulations it checks againstDated, linked to the primary source; not legal advice

Regulation watch

Loading the watch status…

15 laws, rules and guidance pages cited; 15 watched nightly at the primary source. A change marks this page for a human re-check; nothing is edited automatically. What we cite and how it is watched

Technical detailsModels, where it runs, labels

In short

Last reviewed

What it is
Clear a screening alert field by field, with a rule-based proposal, a rationale checked against the fields, and a signed record that pins the list version.
Who it's for
Teams in finance and insurance and compliance and trust.
Where it runs
Self-host for real alerts; the hosted demo takes synthetic customers only (list entries are real public data)
Key numbers
  • 0 of 1,094 Same-party pairs proposed as false positive (the risky direction) (test split, n = 1094)
  • 635 / 635 (100%) False-positive clearance precision (test split, n = 635)
  • 863 (78.9%) Same-party pairs proposed as true match (test split, n = 1094)
  • 8.4 s Median end-to-end run, hosted (QA sweep 2026-09-26)
All results, datasets and caveats
Models
Qwen3.8-27B explains; the comparison, the proposal and the checks are plain code
Where
Self-host for real alerts; the hosted demo takes synthetic customers only (list entries are real public data)
Checks
Receipt per model call, embedded in a signed, hash-chained record with the list snapshot's hash; every rationale sentence checked against its fields
Output
Signed record or verdict · Structured data
Data
Personal data · Confidential business data
Hardware
1× 96 GB GPU
Licence
Permissive (Apache-2.0, MIT)
Runs in
Self-host
Ask a question or leave feedbackWe read every message and publish useful answers
Questions & feedback

Ask about Sanctions alert disposition record

We read every message. Questions, comments and our answers show here once we have reviewed and approved them.

Loading questions…

This is a

Plain text. Please leave out personal, patient or client data.

Shown with your message if we publish it. Leave blank to post as “A visitor”.

Nothing appears here until we have read and approved it.