@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
Maintainers
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/protectZero 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 riskA 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
