Docs menu

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.

View as Markdown

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 checks, 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.
{
  "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 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.
  • Personal data in the state. Dex stores no request content by default. Still, send only what the checks need. See Data handling.