@decentrys/sentinel-sdk
v0.1.3
Published
Monitoring for deployed contracts, wallets and treasuries: targets, detection rules, alerts, and audit-to-monitoring handover.
Maintainers
Readme
@decentrys/sentinel-sdk
Watches contracts, wallets and treasuries you already deployed, and tells you when something changes.
An audit is a point-in-time review. Sentinel is what happens afterwards: you register what you own, write rules about what would worry you, and get alerts when those conditions actually occur.
Install
npm install @decentrys/sentinel-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. |
Sentinel needs a secret key. A key beginning dk_pub_ throws at
construction — configuring detection is not something a credential shipped
inside a client may do.
Quick start
import { Sentinel } from '@decentrys/sentinel-sdk';
const sentinel = new Sentinel({ apiKey: process.env.DECENTRYS_API_KEY! });
const projectId = 'proj_…';
// 1. What do you want watched?
const vault = await sentinel.registerContract({
projectId, chain: 'ethereum', address: '0xabc…', label: 'Vault v2',
});
await sentinel.registerTreasury({
projectId, chain: 'ethereum', address: '0xdef…', label: 'Ops treasury',
});
// 2. What would worry you?
const rule = await sentinel.createRule({
projectId,
name: 'Large treasury outflow',
severity: 'HIGH', // INFO | LOW | MEDIUM | HIGH | CRITICAL
triggerType: 'treasury.transfer',
conditions: {
op: 'AND',
conditions: [
{ op: 'eq', field: 'transfer.direction', value: 'OUT' },
{ op: 'gt', field: 'transfer.valueUsd', value: 50_000 },
],
},
actions: [
{ type: 'ALERT', config: {} },
{ type: 'SLACK', config: { webhookUrl: process.env.SLACK_WEBHOOK_URL } },
],
cooldownSeconds: 300, // suppression window; without one, a loop pages you 1000 times
});
// 3. Read what fired.
const { data: alerts } = await sentinel.listAlerts({ status: 'NEW', limit: 50 });
for (const alert of alerts) {
alert.severity;
alert.title;
alert.ruleName;
alert.facts; // the facts that made the rule fire — an alert without these is unactionable
alert.txHash;
await sentinel.acknowledgeAlert(alert.id, 'Expected — scheduled rebalance.');
}await sentinel.catalog() returns every fact field, operator and action you can
build a rule from, so you can generate a rule builder rather than hardcode one.
Test a rule before you arm it
const test = await sentinel.testRule(rule.id, {
'transfer.direction': 'OUT',
'transfer.valueUsd': 75_000,
});
test.matched; // true
test.explanation; // string[] — which conditions passed and which did notA rule nobody has tested is a rule whose behaviour you'll discover during an incident.
Report events we can't see
Some things only your system knows: a deploy pipeline running, an internally approved treasury movement, an off-chain governance action.
const recorded = await sentinel.reportEvent({
projectId,
eventName: 'governance.executed',
chain: 'ethereum',
txHash,
facts: { 'governance.executed': true, proposalId: 42 },
});
if (!recorded) console.warn('Sentinel did not record the event.');This is the one method that never throws. It runs on your hot path, and a monitoring call must not be able to fail the thing it's monitoring. It returns a boolean instead.
Turn an audit into monitoring
The most valuable thing here. An audit identifies what a contract can do; this converts that into what gets watched.
import { rulesFromAudit, unmappedCapabilities, type AuditCapability } from '@decentrys/sentinel-sdk';
const capabilities: AuditCapability[] = [
{ type: 'UPGRADEABLE', grantedBy: 'EIP-1967 implementation slot' },
{ type: 'MINT_AUTHORITY' },
{ type: 'FEE_CONTROL' },
];
for (const { capability, rule } of rulesFromAudit({
projectId,
capabilities,
treasuryThresholdUsd: 50_000, // omit and no treasury rule is generated
})) {
console.log(`${capability} -> ${rule.name} (${rule.severity})`);
await sentinel.createRule(rule);
}
// Told, not hidden: which capabilities produced no rule.
console.log(unmappedCapabilities(capabilities)); // ['FEE_CONTROL']It watches the upgrade happening, never the contract being upgradeable — a capability is not a fault. Capabilities with no plausible response generate no rule, because a rule nobody can act on trains a team to close alerts unread. A customer handed six rules for nine capabilities is told which three are unwatched, so the gap is a decision rather than an assumption.
Mapped capability names (aliases in brackets): UPGRADEABLE (PROXY,
UPGRADE_AUTHORITY, DELEGATED_EXECUTION) · ADMIN_CONTROL (ADMIN) ·
OWNERSHIP (OWNER) · MINT_AUTHORITY (MINT) · PAUSABLE (PAUSE) ·
BLACKLIST (FREEZE_AUTHORITY, ACCOUNT_FREEZE) · FORCED_BALANCE_CHANGE ·
GOVERNANCE. Anything else comes back from unmappedCapabilities.
No threshold is guessed on your behalf: omit treasuryThresholdUsd and there
is no treasury rule. A guessed threshold is either so low it pages constantly or
so high it never fires, and both teach people to ignore it.
Every method
| | Method |
|---|---|
| Targets | registerContract(input) · registerWallet(input) · registerTreasury(input) · registerTarget(input, targetType) · listTargets(projectId?) · setTargetEnabled(id, enabled) · removeTarget(id) |
| Rules | createRule(input) · updateRule(id, changes) · deleteRule(id) · listRules(projectId?) · testRule(id, facts) · catalog() |
| Alerts | listAlerts({status?, severity?, limit?}) · getAlert(id) · acknowledgeAlert(id, note?) |
| Reporting | reportEvent(event) |
registerTarget takes any TargetType: CONTRACT · WALLET · TREASURY ·
MULTISIG · LP_POOL · ORACLE · BRIDGE · GOVERNANCE. Target input is
{ projectId, chain, address, label? }.
It throws
Unlike @decentrys/protect, this client raises errors. It's management — if
registering a contract fails, you must know, or you'll believe you're monitored
when you aren't.
import { Sentinel, SentinelError } from '@decentrys/sentinel-sdk';
try {
await sentinel.registerContract({ projectId, chain: 'ethereum', address });
} catch (error) {
if (error instanceof SentinelError) console.error(error.status, error.code, error.message);
}SentinelError 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 Sentinel({ apiKey, timeoutMs, baseUrl, fetch }).
reportEvent is the single exception and returns false instead.
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
