Appearance
Merchant KYB onboarding
The development seed as a policy pack: the kyb-onboarding workflow, the rule set it scores with and every rule that set names, the kyb-basic collection flow and the blocked_countries list. It is the policy the first-decision tutorial walks through, in a file. Pack: examples/policies/merchant-kyb-onboarding/.
This pack is generated from packages/db/src/seed-data.ts by pnpm policy:pack:seed (scripts/policy-pack-from-seed.mjs) and a test (packages/db/src/seed-pack.test.ts) fails when the two drift. Unlike the other packs it deliberately shares the seed's keys: importing it into the seeded development tenant re-creates the seeded definitions as new draft versions (version 2 of each), and --publish makes those the live ones. Into an empty tenant it is the quickest way to get the tutorial's policy.
A pattern for expressing an onboarding policy, not compliance guidance: the supported countries, weights and bands are the seed's illustrative values. The pack screens with the deterministic mock-sanctions app and verifies documents with doc-verify-mock, both of which the development seed installs; a tenant without those installs can import the pack, but its runs fail on unknown_app until both are installed and enabled.
The decision it automates
Whether a business applying for an account is approved, rejected or handed to a reviewer, from what the applicant enters, a sanctions screen of the legal name and, when the owner uploads an identity document, the verifier's verdict.
Data expected
- A subject of kind
merchantwithdata.country(the country the business was registered with the platform under). - The
kyb-basiccollection flow: business details (legalName,country,entityType,website), the beneficial owner for companies (uboName,uboOwnershipPct,uboCountry; sole traders skip the step), a confirmation and an optionalidentityDocumentfile.
Steps and rules
| Step | Type | Why |
|---|---|---|
collect | wait_for_collection | The run pauses until the applicant submits kyb-basic; the data lands under submission.*. |
screen | call_app | mock-sanctions screen on submission.legalName and submission.country; output under sanctions. |
has_document | branch | Only when submission.identityDocument.fileId exists does the run pay for a verification. |
idv | call_app | doc-verify-mock verify on the uploaded file and the owner's name; asynchronous: the run waits for the vendor's signed webhook. |
rules | evaluate_rules | The kyb-onboarding rule set. |
route | branch | rules.outcome == manual_review opens a case; anything else decides at once. |
review | create_case | kyb_review, priority high. |
decide | emit_decision | From the rules, or from the reviewer's decision when there was a case. |
The rule set kyb-onboarding sums weights and maps the score through bands 0-40 approve, 40-90 manual_review, 90-100 reject:
| Rule | Type | Severity, weight | Fails when |
|---|---|---|---|
country_allowed | comparison | block, 100 | subject.country is not US, GB or DE |
submitted_country_allowed | comparison | warn, 50 | the country entered in the flow is not US, GB or DE |
country_blocklisted | list_lookup | block, 100 | the entered country is on blocked_countries (seeded with KP and IR) |
sanctions_hit | app | warn, 50 | the mock screen reports hit (the seeded install blocklists Evil Corp) |
high_ownership_foreign_ubo | expression | warn, 30 | an owner holds 75 % or more from a country other than the business (hasPath guards sole traders) |
identity_not_verified | expression | warn, 40 | a document was verified and the verdict is not approved; passes when no document was uploaded |
With these weights a blocklisted country (100 + 50) clamps to 100 and rejects; a sanctions hit plus a foreign owner (80) lands in review; an unverified document alone (40) lands in review.
What a reviewer sees
A kyb_review case with the submission, the sanctions matches, the verifier's checks when a document was uploaded, and every rule result with its score. The reviewer's decision becomes the run's decision (source: manual), with the rule results attached.
How to adapt it
Change the country lists and the bands in the rule set. Screen with the opensanctions app of the App catalogue instead of the mock, once its apiKey secret is stored: the screen step and the sanctions_hit rule name it, with input: { "type": "Company" } (the seed variant SEED_SANCTIONS_APP=opensanctions shows the exact edits). Verify documents with the sumsub app instead of doc-verify-mock, once its three secrets are stored: its verify takes the same documentId and the applicant as firstName, lastName and dateOfBirth, so map the owner's name into those. Add fields to kyb-basic and rules that read them under submission.*.
Run it
bash
pnpm policy:import examples/policies/merchant-kyb-onboarding --publishThe fixture (expect.json) submits Evil Corp from GB with a US owner holding 100 %: sanctions hit (50) and foreign owner (30) score 80, a kyb_review case opens, the check approves it and the run completes with a manual approve.