npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@decentrys/dri-sdk

v0.1.2

Published

Digital Recovery Intelligence: fund tracing, attribution, intervention points and evidence packages. Analytical only — Decentrys never takes custody of assets.

Readme

@decentrys/dri-sdk

Follows stolen funds across hops and chains, and identifies where lawful intervention becomes possible.

Digital Recovery Intelligence. You seed an investigation with the addresses that received extracted funds; it traces outward, attributes the destinations, and names the points where a party who can actually act — an exchange, a custodian, a court — becomes reachable.

What this is not

  • Decentrys never takes custody of assets, and does not freeze, seize, transmit or return anything. Nothing in this package can be called to move value.
  • It is not a law firm, law enforcement agency, or licensed recovery agent. Nothing here is legal advice.
  • The Recovery Index is an analytical estimate from a versioned model. Not a prediction, guarantee, or promised recovery rate.
  • Attribution beyond a mixer is inference, and labelled as inference — observedPath: false on every point that reaches you through obfuscation.
  • Sold to organisations. Not a consumer service.

Install

npm install @decentrys/dri-sdk

Zero 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 — an investigation names third parties, and that must never run behind a credential anyone who downloads your app can read.

Quick start

import { DecentrysDri, custodialPoints, inferredPoints } from '@decentrys/dri-sdk';

const dri = new DecentrysDri({ apiKey: process.env.DECENTRYS_API_KEY! });

// 1. Open the case, seeded on where the funds went. Nothing is traced yet —
//    the seed node exists so the graph has a root even if a provider is down.
const graph = await dri.createInvestigation({
  title: 'Vault drain 2026-09-04',
  chain: 'ethereum',
  address: '0xattacker…',
  maxHops: 4,               // 1-4
});

graph.investigationId       // ← the handle every other call takes
graph.caseRef
graph.recoveryCaseId        // string | null — null means no tranche ledger yet
graph.nodes                 // FlowNode[]
graph.edges                 // FlowEdge[]
graph.notes                 // caveats that ship with the result

// 2. Follow the value. One address, one hop, per call.
const trace = await dri.traceFunds({ investigationId: graph.investigationId });

trace.fromNodeId            // which address was expanded
trace.addressesDiscovered
trace.transfersRecorded
trace.coverage?.truncated   // true when the trace stopped short of the frontier
trace.graph                 // the graph as it now stands

// 3. Where can someone act?
const analysis = await dri.identifyInterventionPoints(graph.investigationId);

createInvestigation takes chain and address — not seedChain/ seedAddress — and returns a FlowGraph whose id field is investigationId, not id.

Tracing is one hop at a time on purpose. Public RPC endpoints are rate-limited, and an exhausted quota reads as "the funds vanished" rather than "we ran out of budget". Call it in a loop for depth, and read coverage.truncated each time:

for (let i = 0; i < 20; i += 1) {
  const step = await dri.traceFunds({ investigationId: graph.investigationId });
  if (step.coverage?.truncated) {
    console.warn(step.coverage.truncationReason);
    break;
  }
  if (step.addressesDiscovered === 0) break;
}

Pass fromNodeId to choose the address yourself. Omit it and the shallowest unexpanded node is taken, which keeps the graph anchored in what was directly traced.

Intervention points

identifyInterventionPoints returns an InterventionAnalysis with three lists — not custodial/crossChain/obfuscation properties. The custodial/cross-chain split lives on each point's kind:

// A party HOLDS the value — contactable, serveable, subpoenable.
for (const point of custodialPoints(analysis)) {          // kind === 'CUSTODIAL'
  point.entityName ?? point.label;
  point.address;
  point.hop;
  point.basis;          // why it is on the list, in the terms it may be stated
  point.observedPath;   // false ⇒ the link runs through mixing: inference
}

// A bridge operator holds RECORDS of value that has already moved on.
const crossChain = analysis.interventionPoints.filter((p) => p.kind === 'CROSS_CHAIN');

// Where provable attribution stops. Mixing and privacy infrastructure.
analysis.attributionLimits;   // [{ nodeId, chain, address, nodeType, hop, reason }]

// Frontier addresses nobody has looked past yet.
analysis.unexplored;

// The points connected to this case by inference rather than an observed transfer.
inferredPoints(analysis);     // observedPath === false

Collapsing custodial and cross-chain sends legal budget after a paper trail. A case that reaches an attribution limit has established an answer, not failed.

Recovery Index

getRecoveryIndex takes a recoveryCaseId, which you get from listCases() or from graph.recoveryCaseId.

import { MissingDisclosureError } from '@decentrys/dri-sdk';

const [openCase] = await dri.listCases();

try {
  const index = await dri.getRecoveryIndex(openCase.recoveryCaseId);

  index.score;           // 0-100
  index.band;            // 'HIGH' | 'MODERATE' | 'LOW' | 'VERY_LOW'
  index.bandMeaning;     // stated as a situation, never as a likelihood
  index.factors;         // [{ factor, contribution, note }] — the number, explained
  index.funnel;          // located / at-intervention-point / frozen / recovered / …
  index.analyticalOnly;  // always true
  index.disclaimer;      // must be shown wherever the score is
  index.modelVersion;
  index.inputsHash;      // with modelVersion, makes any figure reproducible

  render(index.score, index.band, index.disclaimer);   // never a score without its caveat
} catch (error) {
  if (error instanceof MissingDisclosureError) {
    // The score arrived unlabelled. Do not render a bare number.
    return;
  }
  throw error;
}

The client refuses a Recovery Index that arrives without analyticalOnly: true and a non-empty disclaimer — assertAnalyticalDisclosure is exported if you want to apply the same check to a payload of your own. MissingDisclosureError is a distinct class from DriError, so a retry-on-transport-error loop can't swallow it. A dashboard renders whatever arrives, and "58 — MODERATE" beside the word recovery is read as a rate.

Evidence packages

Prepared, not sent. Decentrys does not contact counterparties on your behalf unless instructed.

const pkg = await dri.generateEvidencePackage({
  recoveryCaseId: openCase.recoveryCaseId,
  recipientType: 'EXCHANGE',        // 'EXCHANGE' | 'ISSUER' | 'LAW_ENFORCEMENT' | 'COUNSEL'
  recipientName: 'Example Exchange Ltd',
  // recoveryLeadId: '…'            // narrows the package to one destination
});

pkg.content;          // the document, as plain text
pkg.contentHash;      // SHA-256 of content — quote it back to verify what a recipient holds
pkg.authorityNotice;  // the standing non-authority statement on the face of the document
pkg.transmitted;      // always false on a package this SDK created

The document is frozen and hashed at generation, so a reader months later can confirm they hold what was produced. A later version is a new document, not a revision of this one.

Every method

| Method | Returns | |---|---| | createInvestigation({title, chain, address, incidentId?, maxHops?}) | FlowGraph | | listInvestigations(limit?) | InvestigationSummary[] | | traceFunds({investigationId, fromNodeId?}) | TraceResult | | getFlowGraph(investigationId) | FlowGraph | | identifyInterventionPoints(investigationId) | InterventionAnalysis | | getInvestigationTimeline(investigationId, limit?) | InvestigationTimeline | | listCases(limit?) | RecoveryCaseSummary[] | | getRecoveryIndex(recoveryCaseId) | RecoveryIndex | | generateEvidencePackage({recoveryCaseId, recipientType, recipientName, recoveryLeadId?}) | EvidencePackage |

Helpers: custodialPoints(analysis) · inferredPoints(analysis) · assertAnalyticalDisclosure(index) · RECOVERY_BANDS.

Every timeline entry carries actorParty as a required field. An entry saying value "became frozen" with no named actor would be an implied claim of standing Decentrys does not have.

It throws

Nothing here is on a signing path. A trace that quietly returned an empty graph when the service was unreachable would tell a responder the value had vanished at the moment it was still moving — a failure invisible by construction.

import { DriError } from '@decentrys/dri-sdk';

try {
  await dri.traceFunds({ investigationId });
} catch (error) {
  if (error instanceof DriError) console.error(error.status, error.code, error.message);
}

DriError carries status (0 for a timeout or network failure) and the API's own code and message. The default deadline is 30 seconds — a trace expands one address against rate-limited public endpoints, and the graph is persisted server-side, so a timed-out trace is re-readable rather than lost. Configure with new DecentrysDri({ apiKey, timeoutMs, baseUrl, fetch }).

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

decentrys.com · SDK overview · Developer API · Source