diff --git a/shulam/PLUGIN.md b/shulam/PLUGIN.md new file mode 100644 index 0000000..d82c017 --- /dev/null +++ b/shulam/PLUGIN.md @@ -0,0 +1,46 @@ +# Shulam Compliance Plugin for BankrBot Claude Plugins + +## Summary + +Adds two advisory commands and a pre-flight hook to the BankrBot Claude plugin ecosystem: + +- **`shulam verify
`** — Screen a wallet address against OFAC SDN sanctions lists +- **`shulam trust
`** — Get a trust score (0–100) with tier and breakdown +- **x402 pre-flight hook** — Automatic advisory compliance check before x402 payments + +All commands are **advisory-only** — they print warnings but never block or throw errors. + +## How It Works + +1. Commands call the Shulam CaaS API (`POST /v1/compliance/screen`, `GET /v1/trust/score/:address`) +2. Results are session-cached (no duplicate API calls within a session) +3. The x402 pre-flight hook runs automatically before payment submission + +## Configuration + +Set `SHULAM_API_KEY` in your environment. If not set, all checks are silently skipped. + +```bash +export SHULAM_API_KEY=your_key_here +``` + +Free tier: 100 requests/day. Get a key at [api.shulam.xyz/register](https://api.shulam.xyz/register). + +## Status Mapping + +| API Response | Displayed As | Action | +|-------------|-------------|--------| +| `clear` | CLEAR | No action needed | +| `held` | HELD | Advisory warning | +| `blocked` | BLOCKED | Strong warning | +| `pending` | HELD | Advisory warning | + +## Zero Dependencies + +All files use raw `fetch` only. No npm packages required. + +## Links + +- [Shulam Compliance API Docs](https://docs.shulam.xyz/compliance) +- [x402 Protocol](https://x402.org) +- [OFAC SDN List](https://sanctionssearch.ofac.treas.gov/) diff --git a/shulam/commands/shulam-trust.ts b/shulam/commands/shulam-trust.ts new file mode 100644 index 0000000..66c748c --- /dev/null +++ b/shulam/commands/shulam-trust.ts @@ -0,0 +1,94 @@ +/** + * `shulam trust
` — Claude plugin command for trust scoring. + * + * Advisory-only: prints score/tier/breakdown but never throws or blocks. + * Session-cached: repeated lookups are instant. + */ + +// ── Types ────────────────────────────────────────────────────── + +type TrustTier = 'unknown' | 'new' | 'established' | 'trusted' | 'exemplary'; + +interface TrustBreakdown { + volume: number; + reliability: number; + compliance: number; + diversity: number; + longevity: number; + stability: number; +} + +interface TrustResult { + score: number; + tier: TrustTier; + breakdown: TrustBreakdown; +} + +// ── Session cache ────────────────────────────────────────────── + +const sessionCache = new Map(); + +// ── Command ──────────────────────────────────────────────────── + +export async function shulamTrust(address: string): Promise { + const apiKey = process.env.SHULAM_API_KEY; + const baseUrl = process.env.SHULAM_API_URL ?? 'https://api.shulam.xyz'; + + if (!apiKey) { + console.log('[shulam] No SHULAM_API_KEY set — trust check skipped.'); + console.log('[shulam] Get a free key: https://api.shulam.xyz/register'); + return; + } + + // Check session cache + const cached = sessionCache.get(address.toLowerCase()); + if (cached) { + printResult(address, cached); + return; + } + + try { + const response = await fetch( + `${baseUrl}/v1/trust/score/${encodeURIComponent(address)}`, + { + method: 'GET', + headers: { 'X-API-Key': apiKey }, + }, + ); + + if (!response.ok) { + console.log(`[shulam] Trust lookup returned HTTP ${response.status} — skipping.`); + return; + } + + const data = await response.json() as Record; + const passport = (data.passport ?? data) as Record; + const breakdownRaw = (passport.breakdown ?? {}) as Record; + + const result: TrustResult = { + score: (passport.trustScore as number) ?? 0, + tier: ((passport.trustTier as string) ?? 'unknown') as TrustTier, + breakdown: { + volume: breakdownRaw.volume ?? 0, + reliability: breakdownRaw.reliability ?? 0, + compliance: breakdownRaw.compliance ?? 0, + diversity: breakdownRaw.diversity ?? 0, + longevity: breakdownRaw.longevity ?? 0, + stability: breakdownRaw.stability ?? 0, + }, + }; + + sessionCache.set(address.toLowerCase(), result); + printResult(address, result); + } catch { + console.log('[shulam] Trust lookup failed (network error) — advisory only, continuing.'); + } +} + +function printResult(address: string, result: TrustResult): void { + const shortAddr = `${address.slice(0, 6)}...${address.slice(-4)}`; + + console.log(`[shulam] ${shortAddr} — Trust Score: ${result.score}/100 (${result.tier})`); + console.log(`[shulam] Volume: ${result.breakdown.volume} | Reliability: ${result.breakdown.reliability} | Compliance: ${result.breakdown.compliance}`); + console.log(`[shulam] Diversity: ${result.breakdown.diversity} | Longevity: ${result.breakdown.longevity} | Stability: ${result.breakdown.stability}`); +} diff --git a/shulam/commands/shulam-verify.ts b/shulam/commands/shulam-verify.ts new file mode 100644 index 0000000..c7fd4c2 --- /dev/null +++ b/shulam/commands/shulam-verify.ts @@ -0,0 +1,95 @@ +/** + * `shulam verify
` — Claude plugin command for compliance screening. + * + * Advisory-only: prints warnings but never throws or blocks execution. + * Session-cached: repeated checks for the same address are instant. + */ + +// ── Types ────────────────────────────────────────────────────── + +type ComplianceStatus = 'clear' | 'held' | 'blocked'; + +interface ScreeningResult { + status: ComplianceStatus; + matchScore: number; + screenedAt: string; +} + +// ── Session cache ────────────────────────────────────────────── + +const sessionCache = new Map(); + +// ── Status mapping (ADR-23) ──────────────────────────────────── + +const STATUS_MAP: Record = { + clear: 'clear', + held: 'held', + blocked: 'blocked', + pending: 'held', + error: 'held', +}; + +// ── Command ──────────────────────────────────────────────────── + +export async function shulamVerify(address: string): Promise { + const apiKey = process.env.SHULAM_API_KEY; + const baseUrl = process.env.SHULAM_API_URL ?? 'https://api.shulam.xyz'; + + if (!apiKey) { + console.log('[shulam] No SHULAM_API_KEY set — compliance check skipped.'); + console.log('[shulam] Get a free key: https://api.shulam.xyz/register'); + return; + } + + // Check session cache + const cached = sessionCache.get(address.toLowerCase()); + if (cached) { + printResult(address, cached); + return; + } + + try { + const response = await fetch(`${baseUrl}/v1/compliance/screen`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey, + }, + body: JSON.stringify({ address }), + }); + + if (!response.ok) { + console.log(`[shulam] Compliance check returned HTTP ${response.status} — treating as advisory warning.`); + return; + } + + const data = await response.json() as Record; + const result: ScreeningResult = { + status: STATUS_MAP[data.status as string] ?? 'held', + matchScore: (data.matchScore as number) ?? 0, + screenedAt: (data.screenedAt as string) ?? new Date().toISOString(), + }; + + sessionCache.set(address.toLowerCase(), result); + printResult(address, result); + } catch { + console.log('[shulam] Compliance check failed (network error) — advisory only, continuing.'); + } +} + +function printResult(address: string, result: ScreeningResult): void { + const shortAddr = `${address.slice(0, 6)}...${address.slice(-4)}`; + + switch (result.status) { + case 'clear': + console.log(`[shulam] ${shortAddr} — CLEAR (no sanctions match)`); + break; + case 'held': + console.log(`[shulam] ⚠ ${shortAddr} — HELD (match score: ${result.matchScore.toFixed(2)}, under review)`); + break; + case 'blocked': + console.log(`[shulam] ✘ ${shortAddr} — BLOCKED (confirmed sanctions match, score: ${result.matchScore.toFixed(2)})`); + console.log('[shulam] WARNING: Transacting with this address may violate sanctions regulations.'); + break; + } +} diff --git a/shulam/patch/x402-preflight.ts b/shulam/patch/x402-preflight.ts new file mode 100644 index 0000000..df8585c --- /dev/null +++ b/shulam/patch/x402-preflight.ts @@ -0,0 +1,109 @@ +/** + * x402 Pre-flight Compliance Hook + * + * Intercepts x402 payment flows to run an advisory compliance check + * before the payment is submitted. Prints warnings but never blocks. + * + * Integration: Register as a pre-payment hook in the x402 plugin pipeline. + */ + +type ComplianceStatus = 'clear' | 'held' | 'blocked'; + +const STATUS_MAP: Record = { + clear: 'clear', + held: 'held', + blocked: 'blocked', + pending: 'held', + error: 'held', +}; + +// Session-level cache (lives for plugin lifetime) +const preflightCache = new Map(); + +export interface PreflightResult { + address: string; + status: ComplianceStatus; + advisory: boolean; + message: string; +} + +/** + * Run a pre-flight compliance check on the payment recipient. + * Advisory-only: always returns (never throws), prints warnings. + */ +export async function x402Preflight(recipientAddress: string): Promise { + const apiKey = process.env.SHULAM_API_KEY; + const baseUrl = process.env.SHULAM_API_URL ?? 'https://api.shulam.xyz'; + + // No API key — skip silently + if (!apiKey) { + return { + address: recipientAddress, + status: 'clear', + advisory: true, + message: 'Compliance check skipped (no SHULAM_API_KEY). Set one at https://api.shulam.xyz/register', + }; + } + + // Check cache + const cached = preflightCache.get(recipientAddress.toLowerCase()); + if (cached) { + return { + address: recipientAddress, + status: cached, + advisory: true, + message: cached === 'clear' + ? 'Recipient passed compliance pre-flight (cached).' + : `WARNING: Recipient is ${cached} — proceed with caution.`, + }; + } + + try { + const response = await fetch(`${baseUrl}/v1/compliance/screen`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey, + }, + body: JSON.stringify({ address: recipientAddress }), + }); + + if (!response.ok) { + return { + address: recipientAddress, + status: 'clear', + advisory: true, + message: `Compliance pre-flight returned HTTP ${response.status} — proceeding anyway.`, + }; + } + + const data = await response.json() as Record; + const status = STATUS_MAP[data.status as string] ?? 'held'; + + preflightCache.set(recipientAddress.toLowerCase(), status); + + let message: string; + switch (status) { + case 'clear': + message = 'Recipient passed compliance pre-flight.'; + break; + case 'held': + message = 'WARNING: Recipient has a partial sanctions match — compliance review pending.'; + console.log(`[shulam-x402] ⚠ Pre-flight: ${recipientAddress.slice(0, 10)}... is HELD`); + break; + case 'blocked': + message = 'WARNING: Recipient is on a sanctions list. Payment may violate regulations.'; + console.log(`[shulam-x402] ✘ Pre-flight: ${recipientAddress.slice(0, 10)}... is BLOCKED`); + break; + } + + return { address: recipientAddress, status, advisory: true, message }; + } catch { + return { + address: recipientAddress, + status: 'clear', + advisory: true, + message: 'Compliance pre-flight failed (network error) — proceeding anyway.', + }; + } +}