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/protect

v0.1.2

Published

Evidence-based on-chain risk assessment for wallets and dapps. Unknown is neutral, risk requires evidence, and nothing is ever called a scam without one.

Downloads

534

Readme

@decentrys/protect

Tells a user what a transaction actually does, before they sign it — without calling every new project a scam.

You give it a pending transaction, a contract, a token or an address. It returns observed facts, what the code can do, any threat signals with the evidence behind them, and an explicit list of what it could not determine. Your app decides what to do with that.

Lack of evidence is not evidence of malice.

A contract deployed two hours ago by an anonymous wallet with no audit and thin liquidity is unknown, not dangerous. Most tooling scores those facts as risk, which taxes every new project and protects incumbents. There is no code path in this package by which newness, anonymity or obscurity can raise a risk level.

Install

npm install @decentrys/protect

Zero dependencies — package.json declares none. Node 18+, browsers, extensions and React Native. TypeScript types are included.

Getting an API key

Sign in at decentrys.com/developers and create a key.

There are two kinds, and picking the wrong one is the mistake that matters:

| 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. |

dk_pub_test_… and dk_test_… are the same two kinds against test data.

First call

import { Decentrys } from '@decentrys/protect';

const decentrys = new Decentrys({
  apiKey: 'dk_pub_live_...',
  failMode: 'warn',   // what to do if Decentrys is unreachable
  timeoutMs: 4_000,   // a deadline, not a target
});

const result = await decentrys.assessTransaction({
  chain: 'ethereum',
  from: userAddress,
  to: contractAddress,
  data: calldata,
});

Every assessment method resolves to the same ProtectResult. The risk fields live under result.assessment, not on result itself.

const { assessment, decision, subject, cached, demotedSignals } = result;

assessment.riskLevel        // 'CAUTION'
assessment.historyStatus    // 'LIMITED'  ← not a warning; see below
assessment.confidence       // 0.5 — how sure we are of the classification, not how safe it is
assessment.confirmedMalicious
assessment.facts            // ObservedFact[]        — directly verifiable, no accusation
assessment.capabilities     // TechnicalCapability[] — what the code CAN do
assessment.threatSignals    // ThreatSignal[]        — the only thing that raises a level
assessment.unknowns         // UnknownField[]        — stated, never silently omitted
assessment.explanation      // string[] — never empty; no black-box classifications
assessment.components       // technicalRisk, behavioralRisk, threatIntelligenceRisk, historyConfidence
assessment.modelVersion     // 'protect-1.0.0'
assessment.assessedAt

decision.action             // what YOUR policy says to do — advice, never enforced
decision.reason
subject                     // { kind, chain, identifier }
cached                      // served from the local TTL cache rather than the network
demotedSignals              // coverage signals the SDK refused to treat as risk

A response, abridged to the interesting parts:

{
  "assessment": {
    "riskLevel": "CAUTION",
    "confirmedMalicious": false,
    "confidence": 0.5,
    "historyStatus": "LIMITED",

    // Directly verifiable. Carry no accusation.
    "facts": [
      { "type": "DEPLOYED_AT", "value": "2026-09-04",
        "statement": "The contract was deployed on 2026-09-04.",
        "source": "etherscan", "observedAt": "2026-09-06T09:00:00.000Z" },
      // Arrived as a threat signal; demoted here, because newness is history.
      { "type": "NEW_CONTRACT", "value": null,
        "statement": "The contract was deployed recently.",
        "source": "decentrys", "observedAt": "2026-09-06T09:00:00.000Z" }
    ],

    // What the code CAN do. A capability is not a vulnerability.
    "capabilities": [
      { "type": "UPGRADEABLE", "severity": "SIGNIFICANT",
        "statement": "The contract is a proxy: whoever holds its upgrade rights can replace its logic after you approve it." }
    ],

    // The only thing that can raise a risk level. Each carries its evidence.
    "threatSignals": [],

    // Stated, never silently omitted.
    "unknowns": [
      { "field": "simulation", "reason": "INSUFFICIENT_DATA",
        "statement": "The transaction was not simulated, so its effect on balances is not known here." }
    ],

    "components": { "technicalRisk": 25, "behavioralRisk": 0, "threatIntelligenceRisk": 0, "historyConfidence": 25 },
    "explanation": [
      "The contract is a proxy: …",
      "Little history is available yet. This is normal for anything recently deployed and is not a risk finding.",
      "1 attribute is unknown. Unknown is reported as unknown; it does not contribute to risk."
    ],
    "modelVersion": "protect-1.0.0",
    "assessedAt": "2026-09-06T09:00:00.000Z"
  },
  "decision": { "action": "warn", "reason": "The contract is a proxy: …" },
  "subject": { "kind": "transaction", "chain": "ethereum", "identifier": "0xabc…" },
  "cached": false,
  "demotedSignals": ["NEW_CONTRACT"]
}

Wiring it into a wallet

async function beforeSigning(tx: TransactionRequest) {
  // 1. Assess.
  const result = await decentrys.assessTransaction(tx);
  const { assessment, decision } = result;

  // 2. If nothing was checked, say so. Do not show a green tick.
  if (assessment.unknowns.some((u) => u.reason === 'PROVIDER_UNAVAILABLE')) {
    return showUnavailable(assessment.explanation);
  }

  // 3. Say what it does, in words.
  const explained = await decentrys.explainTransaction(tx);
  explained.summary;    // "Grant 0x1111… unlimited permission to spend the token at 0xabc… from your wallet."
  explained.actions;    // each distinct thing it does, in order
  explained.exposure;   // what the signer gives up if this is not what they intended
  explained.undecoded;  // anything the decoder could not resolve, stated as unresolved

  // 4. Act on YOUR policy, not ours.
  switch (decision.action) {
    case 'allow':                return sign();
    case 'inform':               return showDetails(result);
    case 'warn':
    case 'warn_strong':          return showWarning(result);
    case 'require_confirmation': return showWarning(result, { requireTypedConfirm: true });
    case 'block':                return refuse(result);
  }
}

Render it with @decentrys/ui-sdk if you don't want to build the UI yourself.

Every method

| Method | Returns | Answers | |---|---|---| | assessTransaction(tx) | ProtectResult | Is this transaction worth warning about? | | screenApproval({chain,owner,spender,token,amount?}) | ProtectResult | What am I granting, and to whom? | | scanContract({chain,address}) | ProtectResult | What can this contract do — upgrade, mint, pause, freeze? | | screenToken({chain,address}) | ProtectResult | What is this token, and what powers does it hold? | | screenAddress({chain,address}) | ProtectResult | What is known about this address? | | assessDapp({origin,chain?}) | ProtectResult | Is this site reported phishing infrastructure? | | explainTransaction(tx) | TransactionExplanation | What does it actually do, in plain language? | | simulateTransaction(tx) | SimulationResult | What would change if I signed it? | | getThreatSignals({chain,address}) | ThreatSignal[] | Signals only, for your own presentation. | | clearCache() | void | Drop cached evidence after a user reports a stale result. |

Every one of them takes an optional second argument: { signal?: AbortSignal, skipCache?: boolean }.

const sim = await decentrys.simulateTransaction(tx);
sim.outcome;           // 'SUCCESS' | 'REVERT' | 'NOT_SUPPORTED' | 'UNAVAILABLE'
sim.balanceChanges;    // [{ address, asset, symbol?, decimals?, delta, usdValue? }]
sim.approvalChanges;   // [{ owner, spender, token, symbol?, amount, unlimited }]
sim.contractsCalled;   // in call order
sim.unavailableReason; // stated, rather than an empty result that reads as "nothing happens"

An unrecognised outcome resolves to UNAVAILABLE, never SUCCESS. Defaulting the other way would let a malformed response read as "this transaction is fine".

Configuration

new Decentrys({
  apiKey: 'dk_pub_live_...',   // required
  failMode: 'warn',            // 'open' | 'warn' | 'closed'      (default 'warn')
  timeoutMs: 4_000,            // per attempt                     (default 4000)
  retries: 1,                  // idempotent lookups only         (default 1)
  policy: { HIGH_RISK: 'block', CAUTION: 'inform' },   // merged over DEFAULT_POLICY
  cacheTtlMs: 120_000,         // default 120000
  cacheMaxEntries: 500,        // default 500
  baseUrl: 'https://api.decentrys.com',
  fetch: myFetch,              // injected, so this works in an extension, RN or a test
  transport: myTransport,      // route through your own gateway
});

assessTransaction and screenApproval are never cached: the same spender with an unlimited allowance and with a one-off allowance are different decisions.

Two guarantees

No method throws. This runs between a user and a signing screen. If Decentrys is unreachable you get an assessment saying exactly that in unknowns, with confidence: 0 and historyStatus: 'NONE' — never a reassuring result it didn't earn, and never an exception your wallet has to catch mid-signature.

const { assessment } = await decentrys.screenAddress({ chain: 'ethereum', address });
if (assessment.unknowns.some((u) => u.reason === 'PROVIDER_UNAVAILABLE')) {
  // Nothing was checked. Say so; don't show a green tick.
}

The constructor is the exception, and deliberately: it throws on a missing apiKey, or when no global fetch exists and none was passed. Those are wiring errors a developer makes at deploy time, not events during a user's transaction.

It never blocks. Decentrys returns intelligence; your policy decides. The default (DEFAULT_POLICY, exported) blocks only KNOWN_MALICIOUS, which requires analyst-verified evidence because it's the one output that accuses a third party.

| Level | Default action | |---|---| | NO_CRITICAL_RISK_DETECTED | allow | | INFORMATIONAL | inform | | CAUTION | warn | | ELEVATED_RISK | warn_strong | | HIGH_RISK | require_confirmation | | CRITICAL_THREAT | require_confirmation | | KNOWN_MALICIOUS | block |

Nothing in this package acts on those words. decision.action is a string your code switches on.

Risk levels

NO_CRITICAL_RISK_DETECTED · INFORMATIONAL · CAUTION · ELEVATED_RISK · HIGH_RISK · CRITICAL_THREAT · KNOWN_MALICIOUS

Capabilities can reach CAUTION. Only threat signals with evidence go past it, and only signals that are ACTIVE and at least MIN_RAISING_CONFIDENCE (0.5, exported) count — a stale or low-confidence signal is still reported, with the explanation saying why it did not raise the level.

RISK_LEVEL_MEANING[level] gives wording safe to show a user; isAtLeast(level, floor) compares two levels.

historyStatus: 'LIMITED' is not a warning. It measures how much Decentrys knows, never the subject's danger, and is the correct state for anything recently deployed. HISTORY_STATUS_MEANING[status] says so in words. Render it neutrally.

Coverage signals — NEW_CONTRACT, NO_AUDIT, ANONYMOUS_DEPLOYER, LOW_LIQUIDITY and the rest — are demoted to facts at the wire boundary no matter who sends them, and listed in result.demotedSignals so you can watch the rule being applied.

Classification happens on your side

The API returns evidence; classify() runs in this package, in code you can read. That is why the rule above is a property you can audit rather than a promise on a marketing page — and why a future server change cannot undo it.

Browsers and extensions

<script src="node_modules/@decentrys/protect/dist/browser/decentrys-protect.js"></script>
<script>
  const decentrys = new DecentrysProtect.Decentrys({ apiKey: 'dk_pub_live_...' });
</script>

ESM at @decentrys/protect/browser. Both bundles are dependency-free, for pages with a strict CSP. Use a publishable key here — never dk_live_.

Read the tests

src/classify.test.ts ships inside the package and encodes the cases that must never regress — including that a brand-new unaudited contract from an anonymous deployer is INFORMATIONAL, that two hours of age contributes nothing to technical risk, and that no absence of reputation can reach KNOWN_MALICIOUS. You don't have to take the claim on trust.

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