# Changelog of Sanctions Check — OFAC SDN Screening (`mfapitools/sanctions-check`) Actor

- **URL**: https://apify.com/mfapitools/sanctions-check/changelog.md
- **Full Actor documentation**: https://apify.com/mfapitools/sanctions-check.md

## Changelog

### 0.1.5 — 2026-07-24 (publish-prep: store metadata + README revamp, Claude Sonnet worker agent)

- **Store metadata set via API** (actor stays PRIVATE, `isPublic: false` unchanged): `title` → "Sanctions Check — OFAC SDN Screening", `description` (277 chars, value-prop + pay-per-event), `seoTitle` (50 chars), `seoDescription` (151 chars), `categories` → `["BUSINESS", "AUTOMATION", "AGENTS"]` (no GOVERNMENT/DATA/FINANCE category exists on the platform), `exampleRunInput` → `{"mode":"query","query":"MADURO","searchType":"all","maxResults":10}`. Every field set and GET-verified individually via `apify api PUT acts/rydQC6qeiSyc0DdhZ`.
- **README revamped** to the store-listing quality bar (matching `trustpilot-reviews-scraper/README.md`): added a pay-per-event value-prop lead, expanded use cases (KYC/customer onboarding, supplier due diligence, AI-agent MCP tool, LatAm due-diligence pairing with `cuit-lookup`/`licitaciones-feed`), a real example output record (the "MADURO MOROS, Nicolas" match from the build 0.1.4 platform validation run), a corrected pricing table and real-world cost examples reflecting that each `query` is one run (input takes a single string, not a batch array), an "Use with AI agents" / MCP section, and a dedicated Compliance section (OFAC SDN source URL, public-domain status, update cadence caveat). No code changes; `npm run build` re-run for hygiene (bundle output unchanged in content).
- **Actor-start pricing derived from `defaultMemoryMbytes: 2048`** (2 GB) using the platform's documented "1 event per GB, min 1" rule: recommend console `eventPriceUsd` of $0.0005 for `apify-actor-start`, giving a $0.001 total actor-start charge per run — sized to roughly cover the measured ~$0.0004-0.0005/query compute cost (see 0.1.4 entry) rather than the much larger ~$0.03 `refresh` compute cost, since `refresh` is intended as a scheduled/operational action, not a per-customer-query cost. Full PPE event table (event keys, titles, descriptions, prices) written to `handoff/sanctions-check-publish.md` for the owner's console session — **not yet applied**, since PPE pricing/publish requires the owner (agents cannot set pricing or publish per repo guardrails).
- **Icon still missing** (`pictureUrl: null`): `scripts/upload-icons.ts`'s token-in-URL approach to Apify Console does not authenticate — `https://console.apify.com?token=...` redirects straight to `/sign-in` (verified this session with a scoped copy of the script run against sanctions-check + sentinel-monitor only). Apify Console uses a real login session, not the API token, so this needs either an interactive browser session (owner logs in once, script reuses the session/cookie) or manual upload via Console → Actor → Settings → General → icon upload. Icon file is ready at `.actor-icons/sanctions-check.png`.

### 0.1.4 — 2026-07-24 (index redesign, Claude Sonnet worker agent)

- **Fixed the publish-blocking bug from 0.1.3:** query mode's shard selection was structurally incapable of finding partial-name matches (records sharded by `sha256(full name)`, queries sharded by `sha256(query text/words)` — the two essentially never collide). Redesigned the index as a **token-based inverted index**: `src/index-store.ts` now normalizes each record's name, aliases (AKAs), address city/country, and ID numbers into uppercase, diacritic-stripped tokens at ingest, and writes a `(token, uid)` ref into a token shard (`tok-<2 hex>`, 256 shards, keyed `sha256(token)`) for every distinct token. Full record data is written exactly once per record into a separate record shard (`rec-<2 hex>`, 256 shards, keyed `sha256(uid)`) — this keeps the index size close to the raw record size instead of duplicating a full record once per token (records average ~6-8 distinct tokens; naive per-token duplication would have inflated KVS bytes written ~6-8x). Query mode tokenizes the query, unions candidate uids across all query-token shard hits (recall-favoring — a screening tool shouldn't silently drop a candidate that only matched one query word), fetches the specific candidate records (grouped by record shard to minimize reads), and reuses the existing `scoreMatch`/substring-fuzzy logic (extracted into a new `scoreRecord` function) to rank and filter by `minScore`.
- The old `shard-<hex>` KVS keys from the v1 format are left in place as harmless orphaned dead weight (not actively purged, to avoid ~256 extra KVS reads on every refresh); `shardKeyForName` is retained only as `legacyShardKeyForName` for documentation/history, no longer used by main.ts.
- **Honest limitation documented:** shard *retrieval* is by whole token, not arbitrary substring — a query for "MADUR" (a sub-word fragment that is never itself a complete word in the index) will not retrieve candidates, even though "MADURO" is indexed. Once a candidate is retrieved via any full-token hit, substring/prefix scoring still applies across the whole query string for ranking. See README "Honest limitations".
- **Tests:** added 41 new tests (`tests/sanctions-check.test.ts` grew from 47 → 88 tests; combined with the unchanged 5 in `tests/download.test.ts`, suite total is 93, up from 52) covering: tokenization (`normalizeToken`, `tokenize`, diacritics, punctuation, dedup, min-length), `tokensForRecord` (name/alias/address/ID token extraction), shard-key functions (`recordShardKey`, `tokenShardKey`, KVS charset legality), token-shard codecs and `findUidsForToken`, `scoreRecord`, and a full **end-to-end regression suite** (`describe('token-index end-to-end query pipeline')`) that builds an in-memory index via the exact ingest pipeline and proves: (1) querying `"MADURO"` now finds the `"MADURO MOROS, Nicolas"` record (the exact case proven broken live in 0.1.3) with a sensible score, (2) retrieval also works via other name words and alias tokens, (3) multi-word queries union candidates, (4) unrelated records are correctly excluded, (5) a synthetic negative-control name (`"ZZQXWV FOOBARIAN"`) returns zero results cleanly, and (6) the documented sub-word-fragment limitation behaves as specified. All 93/93 tests green, typecheck clean.
- **Platform validation (build 0.1.4, 3 of 4-run budget used):**
  - **Run 1 — full refresh** (`uuroUyxKCbxgvm49m`): downloaded 27.5 MB, parsed 19,241 entries → 256 record shards (11.05 MB) + 256 token shards (9.70 MB, 308,017 token refs), exit 0/SUCCEEDED. Duration 43.8s / cost $0.0308 — **materially worse than the 0.1.3 baseline (22.5s / ~$0.016)**, roughly 2x, because this refresh now writes 512 KVS shards instead of 256 (record shards + token shards vs. one combined shard). Still cheap in absolute terms (~$0.03/refresh; a daily scheduled refresh costs ~$1/month) and refresh is not currently a billed event.
  - **Run 2 — query "MADURO"** (`ZmZvQSUWXes0L1dH8`): **4 matches**, including `"MADURO MOROS, Nicolas"` (uid 22790, VENEZUELA + IRAN-CON-ARMS-EO programs) — the exact record proven unreachable in 0.1.3. Also correctly surfaced `"MADURO GUERRA, Nicolas Ernesto"`, `"FLORES DE MADURO, Cilia Adela"`, and `"FELIX B. MADURO S.A."` — all genuine OFAC records containing the token MADURO. Exit 0/SUCCEEDED, duration 3.3s, cost $0.00048.
  - **Run 3 — negative control "ZZQXWV FOOBARIAN"** (`4dMt9h3CY1CXRZZxn`): 0 candidate uids, 0 matches, clean exit 0/SUCCEEDED (not an error). Duration 2.2s, cost $0.00031.
  - **Run 4 held in reserve, unused** — no fix-and-retry cycle was needed.
  - **Measured per-query cost:** ~$0.0004-0.0005 in Apify compute-unit terms (excludes the platform's own PPE cut and the `apify-actor-start` memory-based event). At the previously suggested `sanction-screened` price of $0.003, that implies a gross margin of roughly 83-87% — consistent with the prior 85-90% estimate; **no pricing change recommended** based on this data.

### 0.1.3 — 2026-07-24 (platform validation, Claude Sonnet worker agent)

- **Fixed:** `downloadToBuffer` only followed a single HTTP redirect hop. The real OFAC SDN download chain redirects TWICE (`treasury.gov` → `sanctionslistservice.ofac.treas.gov` → signed S3 URL), so every `mode=refresh` run failed on the platform with `Redirect download failed with status 302` (confirmed via platform run `hqffPOqhrtSDShCdp`, build 0.1.2). Extracted the download logic into `src/download.ts` as a loop that follows up to `maxRedirects` (default 5) hops, with dependency-injected `get` for testability. Added 5 regression tests (`tests/download.test.ts`) covering 0/1/2-hop chains, redirect-limit exhaustion, and non-200 terminal status. 52/52 tests green, typecheck clean. Redeployed as build 0.1.3; refresh re-validated on platform (run `zh72vTVaQ3kCtIiHz`): downloaded 27.5 MB, parsed 19,241 SDN entries into 256 KVS shards in 22.5s, exit 0.
- **Found, NOT fixed — publish-blocking:** query mode's shard-selection is structurally broken for its advertised "substring match" / "fuzzy scoring" behavior. Each record's KVS shard key is `sha256(record.name)` where `record.name` is the full `"lastName, firstName"` string, but a query only searches the shard(s) keyed by `sha256(query text)` and `sha256(each query word)`. Since a partial query's hash essentially never equals the hash of a matching record's full name, real substring queries silently return zero matches. Reproduced live: querying `"MADURO"` against the freshly-refreshed index returned 0 matches (platform run `LztCEX0y0RrEkbRoA`, exit 0/SUCCEEDED — a false negative, not a crash), even though "MADURO MOROS, Nicolas" (OFAC SDN, VENEZUELA program) is indexed — confirmed present via direct KVS read of `shard-4e`, while the query's computed shard was `shard-c1`. This is a structural indexing redesign (token/prefix-based sharding or full-scan-and-filter), not a patch, and re-validating a fix would require another refresh + query run beyond this session's 3-run platform budget. **Do not publish until this is redesigned and re-validated.**

### 0.1.0 — 2026-07-23

- Initial build. Refresh mode downloads + indexes OFAC SDN XML. Query mode provides substring name search with fuzzy scoring. 47 tests.
