Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions shulam/PLUGIN.md
Original file line number Diff line number Diff line change
@@ -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 <address>`** — Screen a wallet address against OFAC SDN sanctions lists
- **`shulam trust <address>`** — 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/)
94 changes: 94 additions & 0 deletions shulam/commands/shulam-trust.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* `shulam trust <address>` — 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<string, TrustResult>();

// ── Command ────────────────────────────────────────────────────

export async function shulamTrust(address: string): Promise<void> {
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<string, unknown>;
const passport = (data.passport ?? data) as Record<string, unknown>;
const breakdownRaw = (passport.breakdown ?? {}) as Record<string, number>;

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}`);
}
95 changes: 95 additions & 0 deletions shulam/commands/shulam-verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* `shulam verify <address>` — 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<string, ScreeningResult>();

// ── Status mapping (ADR-23) ────────────────────────────────────

const STATUS_MAP: Record<string, ComplianceStatus> = {
clear: 'clear',
held: 'held',
blocked: 'blocked',
pending: 'held',
error: 'held',
};

// ── Command ────────────────────────────────────────────────────

export async function shulamVerify(address: string): Promise<void> {
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<string, unknown>;
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;
}
}
109 changes: 109 additions & 0 deletions shulam/patch/x402-preflight.ts
Original file line number Diff line number Diff line change
@@ -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<string, ComplianceStatus> = {
clear: 'clear',
held: 'held',
blocked: 'blocked',
pending: 'held',
error: 'held',
};

// Session-level cache (lives for plugin lifetime)
const preflightCache = new Map<string, ComplianceStatus>();

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<PreflightResult> {
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<string, unknown>;
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.',
};
}
}