# Recipe: check messages against a policy

> Check an outgoing message for personal data and promised returns, pick the policy clause that applies and rate the risk, then log the decision and hold the message for a compliance officer.

A financial advisor is about to email a prospect. Before the message leaves, one request checks it against your communication policy: personal data, a promised return, the clause that applies most and the overall risk. When any rule is hit, your code holds the message for a compliance officer and logs what Dex answered.

## The request

- **State.** The message with its channel and audience, and the sender's role.
- **A rubric of checks.** `personal_data` and `return_promise` are separate `check`s, one per rule, so each has its own probability and its own threshold. The `criteria` of `personal_data` spell out, in English, what counts as personal data.
- **`clause` is a `pick`**, so it names the one clause that applies most. When several clauses can apply at once, a check per clause tells you about each of them.
- **`risk` is a `rate`** in beta on three levels. The code only uses it to order the officer's queue.
- **`min_confidence`** of 0.6 on the checks. An unsure check abstains, and the code treats that as a hit: the message waits.

```json
{
  "state": {
    "message": {
      "channel": "email",
      "audience": "prospect",
      "text": "Dear Mr Jansen, following our call: with our Growth Portfolio your savings of 50,000 euros will grow by at least 9 percent a year, guaranteed. I have attached the form with your date of birth and BSN filled in, so you only need to sign. Kind regards, Mark"
    },
    "sender_role": "financial advisor"
  },
  "questions": {
    "personal_data": {
      "type": "check",
      "instructions": "Does {{message.text}} contain personal data about an identifiable person?",
      "criteria": "Personal data includes a national identification number (BSN), a date of birth, an address or financial details tied to a named person.",
      "min_confidence": 0.6
    },
    "return_promise": {
      "type": "check",
      "instructions": "Does {{message.text}} promise or guarantee a financial return?",
      "min_confidence": 0.6
    },
    "clause": {
      "type": "pick",
      "instructions": "Which clause of our communication policy applies most to {{message.text}}?",
      "options": {
        "misleading_claims": "Clause 3: no guaranteed or misleading performance claims",
        "data_protection": "Clause 5: personal data only through the secure portal",
        "conflict_of_interest": "Clause 7: disclose commissions and conflicts of interest",
        "none": "No clause applies"
      },
      "min_confidence": 0.5
    },
    "risk": {
      "type": "rate",
      "instructions": "How much compliance risk does {{message.text}} carry?",
      "levels": ["Low", "Medium", "High"],
      "min_confidence": 0.3
    }
  }
}
```

## Run it

Put a test key in `DEX_API_KEY` (see the [Quickstart](/docs/quickstart/#get-a-key-in-60-seconds)). Save the Python code as `compliance.py` and run `python compliance.py`. Save the TypeScript code as `compliance.mts` and run `npx tsx compliance.mts`: the code uses `await` at the top level, and the `.mts` ending makes the file an ES module. Install the SDKs from [Downloads](/docs/reference/sdks/#downloads).

```bash tab="curl"
curl https://api.thinqit.ai/v1/decide \
  -H "authorization: Bearer $DEX_API_KEY" \
  -H "content-type: application/json" \
  --data-binary @- <<'DEX_REQUEST'
{
  "state": {
    "message": {
      "channel": "email",
      "audience": "prospect",
      "text": "Dear Mr Jansen, following our call: with our Growth Portfolio your savings of 50,000 euros will grow by at least 9 percent a year, guaranteed. I have attached the form with your date of birth and BSN filled in, so you only need to sign. Kind regards, Mark"
    },
    "sender_role": "financial advisor"
  },
  "questions": {
    "personal_data": {
      "type": "check",
      "instructions": "Does {{message.text}} contain personal data about an identifiable person?",
      "criteria": "Personal data includes a national identification number (BSN), a date of birth, an address or financial details tied to a named person.",
      "min_confidence": 0.6
    },
    "return_promise": {
      "type": "check",
      "instructions": "Does {{message.text}} promise or guarantee a financial return?",
      "min_confidence": 0.6
    },
    "clause": {
      "type": "pick",
      "instructions": "Which clause of our communication policy applies most to {{message.text}}?",
      "options": {
        "misleading_claims": "Clause 3: no guaranteed or misleading performance claims",
        "data_protection": "Clause 5: personal data only through the secure portal",
        "conflict_of_interest": "Clause 7: disclose commissions and conflicts of interest",
        "none": "No clause applies"
      },
      "min_confidence": 0.5
    },
    "risk": {
      "type": "rate",
      "instructions": "How much compliance risk does {{message.text}} carry?",
      "levels": ["Low", "Medium", "High"],
      "min_confidence": 0.3
    }
  }
}
DEX_REQUEST
```

```python tab="Python"
# Save as dex_request.py, then run: python dex_request.py
import json

from thinqit_dex import Client

client = Client()  # reads DEX_API_KEY from the environment

request = json.loads(r'''
{
  "state": {
    "message": {
      "channel": "email",
      "audience": "prospect",
      "text": "Dear Mr Jansen, following our call: with our Growth Portfolio your savings of 50,000 euros will grow by at least 9 percent a year, guaranteed. I have attached the form with your date of birth and BSN filled in, so you only need to sign. Kind regards, Mark"
    },
    "sender_role": "financial advisor"
  },
  "questions": {
    "personal_data": {
      "type": "check",
      "instructions": "Does {{message.text}} contain personal data about an identifiable person?",
      "criteria": "Personal data includes a national identification number (BSN), a date of birth, an address or financial details tied to a named person.",
      "min_confidence": 0.6
    },
    "return_promise": {
      "type": "check",
      "instructions": "Does {{message.text}} promise or guarantee a financial return?",
      "min_confidence": 0.6
    },
    "clause": {
      "type": "pick",
      "instructions": "Which clause of our communication policy applies most to {{message.text}}?",
      "options": {
        "misleading_claims": "Clause 3: no guaranteed or misleading performance claims",
        "data_protection": "Clause 5: personal data only through the secure portal",
        "conflict_of_interest": "Clause 7: disclose commissions and conflicts of interest",
        "none": "No clause applies"
      },
      "min_confidence": 0.5
    },
    "risk": {
      "type": "rate",
      "instructions": "How much compliance risk does {{message.text}} carry?",
      "levels": ["Low", "Medium", "High"],
      "min_confidence": 0.3
    }
  }
}
''')

decision = client.decide(
    request["state"],
    request["questions"],
)
for question_id, answer in decision.answers.items():
    print(question_id, answer)
```

```ts tab="TypeScript"
// Save as dex-request.mts, then run: npx tsx dex-request.mts (Node.js 18 or newer)
import { Client, parseRequest } from "@thinqit/dex";

const client = new Client(); // reads DEX_API_KEY from the environment

// parseRequest keeps the key order of the text (JSON.parse would move labels such as "1" to the front).
const request = parseRequest(`{
  "state": {
    "message": {
      "channel": "email",
      "audience": "prospect",
      "text": "Dear Mr Jansen, following our call: with our Growth Portfolio your savings of 50,000 euros will grow by at least 9 percent a year, guaranteed. I have attached the form with your date of birth and BSN filled in, so you only need to sign. Kind regards, Mark"
    },
    "sender_role": "financial advisor"
  },
  "questions": {
    "personal_data": {
      "type": "check",
      "instructions": "Does {{message.text}} contain personal data about an identifiable person?",
      "criteria": "Personal data includes a national identification number (BSN), a date of birth, an address or financial details tied to a named person.",
      "min_confidence": 0.6
    },
    "return_promise": {
      "type": "check",
      "instructions": "Does {{message.text}} promise or guarantee a financial return?",
      "min_confidence": 0.6
    },
    "clause": {
      "type": "pick",
      "instructions": "Which clause of our communication policy applies most to {{message.text}}?",
      "options": {
        "misleading_claims": "Clause 3: no guaranteed or misleading performance claims",
        "data_protection": "Clause 5: personal data only through the secure portal",
        "conflict_of_interest": "Clause 7: disclose commissions and conflicts of interest",
        "none": "No clause applies"
      },
      "min_confidence": 0.5
    },
    "risk": {
      "type": "rate",
      "instructions": "How much compliance risk does {{message.text}} carry?",
      "levels": ["Low", "Medium", "High"],
      "min_confidence": 0.3
    }
  }
}`);

const decision = await client.decide(request);
console.log(decision.answers);
```

## Expected output

```json
{
  "id": "req_01M3JE1ERMJ25WWENPF1Y85BBK",
  "object": "decision",
  "created": 1790546328,
  "model": "dex-1.0.1",
  "served_by": "gpu",
  "calibration": "cal-20260926-1",
  "answers": {
    "personal_data": {
      "type": "check",
      "probability": 0.9517,
      "confidence": 0.9034,
      "abstained": false
    },
    "return_promise": {
      "type": "check",
      "probability": 0.9501,
      "confidence": 0.9001,
      "abstained": false
    },
    "clause": {
      "type": "pick",
      "choice": "misleading_claims",
      "probabilities": {
        "misleading_claims": 0.9396,
        "data_protection": 0.0256,
        "conflict_of_interest": 0.0202,
        "none": 0.0146
      },
      "confidence": 0.914,
      "abstained": false
    },
    "risk": {
      "type": "rate",
      "rating": 1.791,
      "levels": ["Low", "Medium", "High"],
      "probabilities": [0.0243, 0.1605, 0.8152],
      "confidence": 0.5376,
      "abstained": false
    }
  },
  "usage": {
    "input_tokens": 233,
    "state_tokens": 91,
    "question_tokens": 142,
    "allowance_tokens": 0,
    "paid_tokens": 0,
    "charge_micro_cents": 0,
    "unit_price_micro_cents": 0,
    "tier": "test"
  }
}
```

Captured from the live API on 2026-09-27 with a test key: model `dex-1.0.1`, calibration `cal-20260926-1`, `served_by: gpu`, 233 input tokens (91 for the state, 142 for the questions). A test key is charged nothing, so `tier` is `test` and the charge is 0. On this exact version the same request always returns these answers.

| Question | Type | Answer | Confidence | `min_confidence` | Abstained |
| --- | --- | --- | --- | --- | --- |
| `personal_data` | check | yes with probability 0.9517 | 0.9034 | 0.6 | no |
| `return_promise` | check | yes with probability 0.9501 | 0.9001 | 0.6 | no |
| `clause` | pick | `misleading_claims` (0.9396) | 0.914 | 0.5 | no |
| `risk` | rate | rating 1.791, most likely `High` (0.8152) | 0.5376 | 0.3 | no |

The message holds personal data (0.9517) and promises a return (0.9501). The clause that applies most is `misleading_claims` (0.9396), and the risk rating of 1.791 is closest to `High` (0.8152). The email also sends a date of birth and a BSN outside the secure portal, which is clause 5, but a pick names one clause only: `data_protection` got 0.0256. The `personal_data` check catches it anyway.

## Act on it

`message_id` and the functions `log_decision`, `hold_for_officer` and `send_message` stand for your own code. Every response names the model version and the calibration that produced it, and the code logs both with the request id: that is your audit trail. Store the request body with them, and you can replay the decision later on the same version and get the same answers (see [Determinism](/docs/concepts/determinism/#using-it)). Here three rules are hit and the risk is high, so the message waits for an officer, at the top of the queue.

```python tab="Python"
flags = []
for question_id in ("personal_data", "return_promise"):
    answer = decision.check(question_id)
    if answer.abstained or answer.probability >= 0.5:
        flags.append(question_id)  # an unsure check stops the message too
clause = decision.pick("clause")
if clause.abstained or clause.choice != "none":
    flags.append("clause unsure" if clause.abstained else clause.choice)
risk = decision.rate("risk")
urgent = not risk.abstained and risk.rating >= 1.5

log_decision(message_id, decision.id, decision.model, decision.calibration, flags)
if flags:
    hold_for_officer(message_id, flags, urgent)  # a person decides; nothing is sent yet
else:
    send_message(message_id)
```

```ts tab="TypeScript"
const flags: string[] = [];
for (const id of ["personal_data", "return_promise"]) {
  const answer = decision.answers[id];
  if (answer?.type === "check" && (answer.abstained || answer.probability >= 0.5)) flags.push(id); // an unsure check stops it too
}
const { clause, risk } = decision.answers;
if (clause?.type === "pick" && (clause.abstained || clause.choice !== "none")) {
  flags.push(clause.abstained ? "clause unsure" : clause.choice);
}
const urgent = risk?.type === "rate" && !risk.abstained && risk.rating >= 1.5;

logDecision(messageId, decision.id, decision.model, decision.calibration, flags);
if (flags.length > 0) holdForOfficer(messageId, flags, urgent); // a person decides; nothing is sent yet
else sendMessage(messageId);
```

As in the [support recipe](/docs/cookbook/support-routing/#act-on-it), the TypeScript answers have the general `Answer` type, so the code narrows each one on `type`.

## Adapt it

- **Keep a person in the loop.** Here Dex holds a message and a person decides. When a check decides about a person instead, such as refusing a customer, a person must review it: the [acceptable use policy](/legal/acceptable-use/#decisions-about-people) forbids decisions with legal or similarly significant effects on people without human review, in line with article 22 of the GDPR.
- **One check per rule.** Add a `check` for every rule you must enforce, with its definition in `criteria`, in English. All checks share the state, which is billed once.
- **Thresholds from your own cases.** Run messages your officers already judged with a test key, and set the floors so that nothing they would stop gets through. See [Abstention](/docs/concepts/abstention/#choosing-a-threshold).
- **Personal data in the state.** Dex stores no request content by default. Still, send only what the checks need. See [Data handling](/docs/reference/data-handling/).
