Cookbook
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_dataandreturn_promiseare separatechecks, one per rule, so each has its own probability and its own threshold. Thecriteriaofpersonal_dataspell out, in English, what counts as personal data. clauseis apick, 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.riskis aratein beta on three levels. The code only uses it to order the officer's queue.min_confidenceof 0.6 on the checks. An unsure check abstains, and the code treats that as a hit: the message waits.
{
"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). 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.
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# 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)// 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
{
"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). Here three rules are hit and the risk is high, so the message waits for an officer, at the top of the queue.
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)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, 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 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
checkfor every rule you must enforce, with its definition incriteria, 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.
- Personal data in the state. Dex stores no request content by default. Still, send only what the checks need. See Data handling.