Skip to content

High-risk geography ​

Geography review in which rules, a model and a reviewer decide together: two tenant lists (where the business operates, where every onboarding gets a second look), a numeric geography score the integrating system attaches to the subject, a rule set, and a model that reads the merchant's own description of where it trades. Review-band merchants reach a reviewer with a three-day SLA, and so does every merchant the rules would approve unless the model is at least 80% sure it trades in its own country alone. Pack: examples/policies/high-risk-geography/.

A pattern for expressing a geography policy, not compliance guidance. The list rows are placeholders: AQ, BV and UM are uninhabited territories chosen so the example can run without the pack carrying any assessment of a real jurisdiction. Replace them with the tenant's own lists before anything depends on this. The model assesses no country: it reads what the merchant wrote and answers one question about it, while the lists, the score and the reviewer decide.

The decision it automates ​

Whether a merchant from a given country proceeds, is reviewed or is declined, where "which countries" is data that operations maintain through the lists API or the admin console, not a new version of a rule. The model only decides whether an approval needs a person: it never rejects anyone, and a rejection never waits for it.

Data expected ​

  • A subject of kind merchant with data.country (ISO 3166-1 alpha-2) and data.geoRiskScore, a number from 0 to 100 the integrating system computes however it likes (a vendor's country score, an internal model).
  • Optionally data.tradeDescription: the merchant's own words on what it sells and where its customers, suppliers and payments are, from your sign-up form. With it, the model reads it; without it, the run takes the rules alone (the review band to a reviewer, the approve band decided at once) and calls no model.
  • Two lists of kind country: geo_supported_countries and geo_review_countries. The pack creates them with placeholder rows; importing it again adds only rows that are missing.
  • An install named chatgpt (below).

Steps and rules ​

StepTypeWhy
rulesevaluate_rulesThe geo_risk rule set.
routebranchrules.outcome == reject decides at once, without the model; a subject with a tradeDescription goes to read; otherwise manual_review opens a case and anything else decides.
readcall_appchatgpt's decide: the question below, answered under ai.
triagebranchrules.outcome == manual_review opens a case whatever the model said; ai.answers.home_market_only.probability of 0.8 or more decides; anything else opens a case: the model unsure, or sure it trades elsewhere.
reviewcreate_casegeo_review, priority medium, 72-hour SLA: enhanced review, not urgent.
decideemit_decisionFrom the rules, or the reviewer's decision.

The rule set geo_risk sums weights into bands 0-40 approve, 40-90 manual_review, 90-100 reject:

RuleTypeSeverity, weightBehavior
geo_country_supportedlist_lookupblock, 100mode: allow: fails when subject.country is not on geo_supported_countries
geo_country_under_reviewlist_lookupwarn, 60mode: block: fails when the country is on geo_review_countries
geo_risk_scorescore_thresholdwarn, 30contributeScore: true adds subject.geoRiskScore to the risk score whether or not the rule fails; above 70 it also fails

So a review-list country alone scores 60 (review); with a geography score of 20 it is 80 (still review); with 30 or more it reaches 90 and rejects; an unsupported country rejects outright. A missing country fails both lookups; a missing or non-numeric score fails or errors the threshold rule, and an error always routes to review. A merchant in the approve band that described where it trades goes through without a person only when the model is at least 80% sure it trades in its own country alone.

The model step ​

read asks one yes-or-no question about one text, which its stateTemplate writes from the subject's country code and description (Asking for decisions):

text
A merchant registered in the country with the ISO 3166-1 alpha-2 code <<country>> described what
it sells and where it trades, in its own words:
<<tradeDescription>>
QuestionTypeAsks
home_market_onlyyes or no, threshold 0.8Does the description say where the merchant's customers are, with <<country>> the only country it sells to, ships to, buys from or takes payments from?

Its criteria spell out both answers. Yes: "It says where the merchant's customers are, and every country or region it names for its trade is <<country>>." No: "It names another country or region, says the merchant sells or ships internationally or worldwide or takes payments from abroad, or does not say where its customers are."

A yes can only let through a merchant the rules already approve: triage opens the review band's case before it reads the answer, and a rejection never reaches read. The description is the merchant's own text, and a merchant can write anything in it: the question's instructions tell the model to read it as text and never follow instructions in it (Prompt injection), and the most a crafted description wins is what the pack did without a model, an approve-band merchant approved without a person.

What the vendor sees ​

Through the install, OpenAI receives the question with its criteria and the state: the merchant's country code and its description. Nothing else reaches it: not the legal name, the score, the lists or the rule results. The sample install answers decisions through OpenAI's Responses API, which it calls with storing off; an install switched to OpenAI's Decisions API (decideEngine: native) sends the same content under OpenAI's terms for that API (Data protection).

The chatgpt install ​

The pack declares the install its step calls, "installs": { "chatgpt": "chatgpt" }: an install named chatgpt of the platform's chatgpt app, ChatGPT (OpenAI API). An install is never part of a pack. After the import, the console's import result installs chatgpt in one click, with a preset configuration (an OpenAI connection through the Responses API, the console's first suggested model, OpenAI's hosts approved) and a sample key in place of yours; store your OpenAI API key on the install's page (Configuration › Keys › openaiApiKey). Through the API, create it as Installing a model app shows, named chatgpt.

Until you replace the sample key, and whenever the model call fails (an outage that outlasts the retries, an exhausted quota, a refusal), the run fails at read and no case opens: every run with a description that the rules do not reject. Rejections and runs without a description never call the model. OpenAI's answer shows in the invocation record and under the run's app calls.

Every model app answers the same decide, so an install of claude, gemini, jev or openai-compatible named chatgpt serves the pack as well.

What a reviewer sees ​

A geo_review case with the three rule results: which list matched (with the row's reason), the score and the threshold. When the merchant described where it trades, the run's context holds the model's answer under ai (the value, probability and confidence of home_market_only) and Vendor checks lists the call. A no means the description names somewhere the reviewer checks against the lists. Lists are live data: a reviewer who concludes a country no longer needs the second look removes the row, and the next run behaves differently without a new version of anything (every list change is audited).

How to adapt it ​

Fill the lists from your own assessment through the CSV import (POST /lists/:key/import) or the admin console (Define › Lists); give rows an expiresAt for temporary measures. Read the country from a collection flow instead of the subject (submission.country) when the applicant declares it. Read the score from a vendor instead of geoRiskScore: a call_app step through an http-api install for the vendor keeps its answer under the step's outputKey, and the score_threshold points at the score in the vendor's body, such as <outputKey>.body.score (Adding a vendor step). Use inList("geo_review_countries", data.subject.country) inside an expression rule when the geography should only matter in combination with another signal.

Move the model's 0.8 in triage and in the question's threshold together. Give read a fallback on another vendor's install, with the same question, so an OpenAI outage does not fail the runs that reach it (Falling back to another install).

Run it ​

bash
pnpm policy:import examples/policies/high-risk-geography --publish

Then create the chatgpt install and store your key (above). The check runs each fixture against the model stub (scripts/ai-stub.mjs), whose answer a cue word in the description picks ([[stub:low]], [[stub:high]]):

  • expect.json: a merchant in AQ with a geography score of 20, whose description says it ships to Alaska and Chile. Supported passes, the review list fails (60), the score contributes 20 and rules.riskScore is 80; the model answers no (0.05), a geo_review case opens, the check approves it and the run completes with a manual approve.
  • expect-home-market.json: an outfitter in Duluth, Minnesota, that sells only to customers in the United States, with a score of 20. Every rule passes (20, the approve band), the model answers yes (0.95) and the run completes with an automated approve, without a case.

Released under the Apache-2.0 License.