@decentrys/risk-sdk
v0.1.2
Published
Address and transaction screening for exchanges and custodians. Direct and indirect exposure are kept structurally separate, because they are different obligations.
Maintainers
Readme
@decentrys/risk-sdk
Deposit and withdrawal screening for exchanges and custodians, with direct and indirect exposure kept apart.
Install
npm install @decentrys/risk-sdkZero runtime dependencies. Node 18+. Types included.
Getting an API key
Sign in at decentrys.com/developers and create a key.
| Prefix | Where it belongs | Why |
|---|---|---|
| dk_pub_live_… | Publishable. Ships inside a wallet, extension or mobile app. | Bounded to the origins you register and to read-only Protect endpoints. Anyone can extract it from your bundle; that's expected, and it's why it can't do anything dangerous. |
| dk_live_… | Secret. Server-side only. | Full scope access. If this ends up in a client bundle it is a leaked credential the moment it ships. |
Secret key only. A key beginning dk_pub_ throws at construction — a
compliance decision does not belong behind a credential anyone can extract.
Screening a deposit, end to end
import { DecentrysRisk, RiskError } from '@decentrys/risk-sdk';
const risk = new DecentrysRisk({ apiKey: process.env.DECENTRYS_API_KEY! });
const screening = await risk.screenAddress({
chain: 'ethereum',
address: depositAddress,
valueUsd: 25_000, // optional
maxHops: 5, // optional
});
screening.riskLevel; // 'CRITICAL' | 'HIGH' | 'ELEVATED' | 'MODERATE' | 'LOW' | 'UNKNOWN'
screening.riskScore;
screening.confidence;
screening.signals; // ExposureSignal[] — every signal, with its hop count and source
screening.exposure; // the split below
screening.notes; // caveats that ship with the result and must be shown with it
screening.modelVersion;
screening.screenedAt;The distinction this package exists for
const { exposure } = screening;
exposure.direct // ExposureEntry[] — facts about THIS address
exposure.indirect // ExposureEntry[] — observations about counterparties
exposure.hasDirectExposure // boolean
exposure.closestIndirectHop // number | null
// Each entry: { category, closestHop, signalCount, score, signals }"This address is designated" and "value reached it three hops from something designated" are different legal positions. A single blended score forces your compliance team to reconstruct that distinction, and the reconstruction is where it goes wrong.
if (exposure.hasDirectExposure) {
return hold(deposit); // a fact about this address — generally a blocking obligation
}
if (exposure.indirect.length > 0) {
return queueForReview(deposit); // about a counterparty; common and often innocent
}
return credit(deposit);Who is this, and on whose authority?
const { attributions, notes } = await risk.getEntity({ chain: 'ethereum', address: depositAddress });
for (const a of attributions) {
a.label; // 'Binance hot wallet 14'
a.category;
a.entity; // string | null
a.entityType;
a.jurisdiction;
a.confidence; // 0.9
a.source;
a.sourceUrl; // string | null
a.analystJudgement; // false for an OFAC designation, true for our analyst's conclusion
a.observedAt;
}analystJudgement matters: presenting a judgement as a designation is the error
that freezes an innocent customer.
Cross-chain transfers
const bridge = await risk.screenBridgeTransfer({
sourceChain: 'ethereum', sourceAddress: '0xabc…',
destinationChain: 'solana', destinationAddress: 'So1…',
// bridgeAddress, valueUsd optional
});
bridge.combinedRiskLevel; // the HIGHER of the two sides, never an average
bridge.source.riskLevel;
bridge.destination.exposure.hasDirectExposure;A bridge is where provable lineage stops — the two sides are linked by the bridge operator's accounting, not by anything either chain records. Averaging would let a clean destination offset a sanctioned source, which is exactly what someone bridging to launder value relies on.
Watch an address over time
Screening answers a question about a moment. An address clean when you credited a deposit can be designated a week later.
const watch = await risk.monitorAddress({
chain: 'ethereum', address, reference: 'deposit-88213',
});
const watched = await risk.listMonitored();
for (const w of watched) {
w.address; w.reference; w.lastRiskLevel; w.lastScreenedAt; w.nextScreenAt; w.enabled;
}
await risk.stopMonitoring(watch.id);Re-screened daily; alerts only when the result gets worse. Alerting on every
re-screening buries the one that mattered. worsened(from, to) and
RISK_LEVEL_RANK are exported so your own code can make the same comparison.
Every method
| Method | Returns |
|---|---|
| screenAddress({chain, address, maxHops?, valueUsd?}) | AddressScreening |
| screenTransaction({chain, txHash}) | TransactionScreening |
| screenBridgeTransfer({sourceChain, sourceAddress, destinationChain, destinationAddress, bridgeAddress?, valueUsd?}) | BridgeTransferScreening |
| getExposure({chain, address, maxHops?}) | SplitExposure |
| getRiskSignals({chain, address}) | ExposureSignal[] |
| getEntity({chain, address}) | EntityLookup |
| monitorAddress({chain, address, reference?}) | AddressWatch |
| listMonitored() | AddressWatch[] |
| stopMonitoring(watchId) | void |
getExposure and getRiskSignals are screenAddress narrowed to one part of
its result — they cost the same call.
It throws
A screening that quietly returned "nothing found" when the service was
unreachable would credit funds you had a duty to hold — a failure invisible by
construction. So it raises RiskError instead.
try {
await risk.screenAddress({ chain: 'ethereum', address });
} catch (error) {
if (error instanceof RiskError) console.error(error.status, error.code, error.message);
}RiskError carries status (0 for a timeout or network failure) and the
API's own code and message. The default deadline is 10 seconds; configure
with new DecentrysRisk({ apiKey, timeoutMs, baseUrl, fetch }).
Decentrys provides blockchain intelligence. It is not a regulated compliance service and does not replace legal advice or a compliance professional.
The rest of the SDK
| Package | For |
|---|---|
| @decentrys/protect | Pre-sign risk assessment for wallets and dapps |
| @decentrys/ui-sdk | React components that render Protect results |
| @decentrys/sentinel-sdk | Monitoring deployed contracts and treasuries |
| @decentrys/risk-sdk | Screening for exchanges and custodians |
| @decentrys/dri-sdk | Fund tracing and recovery intelligence |
| @decentrys/agent | Policy enforcement for autonomous agents |
Licence
MIT © Decentrys Labs
