Reputation over HTTP
JSON over HTTPS. Every per-address endpoint behaves the same way: looking an address up is a request to index it, and it answers 202 until its history has been read. Nothing is invented for an address without data. A typed TypeScript SDK wraps all of it.
Quick start
curl -H "Authorization: Bearer $TRUST_LAYER_API_KEY" \
$TRUST_LAYER_API/v1/risk/0x…
# 202 while the address is indexed:
{ "subject": "0x…", "indexing": { "status": "backfilling", … } }
# then 200:
{
"status": "scored",
"trust_score": 746,
"band": "strong",
"risk_level": "low",
"confidence": 0.71,
"flags": [],
"score_model_version": "v0.3",
"aleph": null
}const res = await fetch(`${API}/v1/risk/${address}`, {
headers: { Authorization: `Bearer ${process.env.TRUST_LAYER_API_KEY}` },
});
if (res.status === 202) return "pending"; // not rated yet: retry shortly
const risk = await res.json();
if (risk.status !== "insufficient_data" && risk.trust_score < 300) {
warn(risk.flags); // each flag links to its evidence
}API keys
Send your key as a bearer token: Authorization: Bearer tl_live_…. A key is a secret: call the API from your server, never from a browser or a mobile app. A deployment may also answer requests without a key, at a low per-IP rate, or refuse them.
| Status | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | The key is missing where one is required, unknown or revoked. |
| 403 | FORBIDDEN | The key is valid but not allowed to do this, for example an endpoint outside its scopes. |
| 429 | RATE_LIMITED | Too many requests. Wait for Retry-After seconds, then try again. |
The developer dashboard shows a key's limits, today's usage against its quota, the last 30 days and the health of its webhooks.
Rate limits
- Each key has a per-minute limit, and may have a daily quota. Responses carry the current window in
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset(seconds until the window resets). - Over the limit, the API answers
429 RATE_LIMITEDwithRetry-Afterin seconds. Wait at least that long; the SDK does this for you. GET /v1/usagereturns the calling key's limits, today's requests, errors, rate-limited requests and quota left, and the last 30 UTC days.- Batch requests, pagination and webhooks keep request counts low; prefer them to polling many addresses one by one.
HTTP/1.1 429 Too Many Requests
Retry-After: 12
RateLimit-Limit: 120
RateLimit-Remaining: 0
RateLimit-Reset: 12
{ "error": { "code": "RATE_LIMITED", "message": "…" } }Decisions
| GET /v1/risk/:address | Compact answer for integrations: status, score, band, risk level, confidence, medium-or-worse flags, and the latest Aleph assessment as a separate labelled block. |
| GET /v1/trust/:address | The full TRUST RECORD: score, band, risk level, confidence, subscores and the explanation behind every point, caps and confidence factors. |
| GET /v1/trust/:address/history | Append-only score snapshots with why each one changed. ?limit= |
| POST /v1/batch | Up to 100 addresses per request, answered as /v1/risk or /v1/trust. See Batch below. |
Evidence
| GET /v1/rap-sheet/:address | Risk items, newest first. Filters: severity (minimum), classification, type, rule, related, since, until; limit + cursor pagination. |
| GET /v1/record/:address | Record summary (stats, positive lines, risk counts, coverage) plus positive and neutral items, with the same filters. |
| GET /v1/record/:address/positive | Positive lines and positive items only. |
| GET /v1/items/:id | One item by id (evt_…), with its evidence, rule and related entities. |
| GET /v1/token/:address | Token facts: metadata, creation, top holders, pools and liquidity flows, transfer activity. |
| GET /v1/developer/:address | A deploying wallet: everything it launched, how each launch's own record reads, and who funded it. |
Graph & inference
| GET /v1/graph/:address | Neighbourhood over control-like links, funding cluster, and association kept apart from direct evidence. ?depth=1|2&limit=10..300 |
| GET /v1/entities/:address/relationships | Every aggregated relationship with its evidence trail. ?direction=in|out|all&type=FUNDED&limit= |
| GET /v1/aleph/:address | Latest Aleph assessment (ALEPH_INFERENCE) with reasons, resolved evidence refs and confidence factors. Reading it queues a fresh assessment when the evidence changed. |
| GET /v1/aleph/:address/history | Every Aleph run, including rejected ones with the validator's errors. |
Discovery & metadata
| GET /v1/search?q= | Known addresses by full address, hex prefix, token symbol, token name or label. Never queues. |
| GET /v1/entities/:address | Entity and indexing status. 200 indexed, 202 queued or backfilling, 422 for system addresses. |
| GET /v1/rules | Active score model, every detector rule with thresholds, the verified event catalog, the Aleph model and the forbidden-terms list. |
| GET /v1/meta | Entity types, classification vocabulary, score weights and bands. |
| GET /v1/status | Chain connectivity, database, indexer summary, active model versions. |
Keys, webhooks & disputes
| GET /v1/usage | The calling key: name, prefix, scopes, per-minute limit and daily quota, today's usage and quota left, and the last 30 days. |
| POST /v1/webhooks | Subscribe a URL: { url, events[], subjects? }. Answers 201 with the subscription and its signing secret, shown once. |
| GET /v1/webhooks | Your subscriptions (without secrets): events, subjects, active, consecutive failures, disabled reason. |
| DELETE /v1/webhooks/:id | Remove a subscription. |
| POST /v1/webhooks/:id/test | Send a signed ping delivery to the subscription's URL. |
| GET /v1/webhooks/:id/deliveries | Recent deliveries: event, status, attempts, your endpoint's last HTTP status and error. |
| POST /v1/disputes | Report a false positive or incorrect data: { subject, event_id?, kind, reason, contact? } → { id, status }. |
| GET /v1/disputes/:id | Where a dispute stands: open, upheld, rejected or withdrawn. |
Batch
POST /v1/batch takes 1 to 100 addresses and a view of risk (the default) or trust. Every address gets its own entry, in request order, with the status the single endpoint would have answered: 200 with the same data as GET /v1/risk or GET /v1/trust, 202 while it is indexed, or 400, 422, 503 with an error. One bad address never fails the others. The SDK splits longer lists into consecutive requests.
POST /v1/batch
Authorization: Bearer tl_live_…
Content-Type: application/json
{
"addresses": ["0xAb12…", "0x9f8E…", "0xnot-an-address"],
"view": "risk"
}{
"results": [
{ "address": "0xAb12…", "status": 200,
"data": { "status": "scored", "trust_score": 746, … } },
{ "address": "0x9f8E…", "status": 202,
"data": { "indexing": { "status": "queued", … } } },
{ "address": "0xnot-an-address", "status": 400,
"error": { "code": "INVALID_ADDRESS", "message": "…" } }
]
}Webhooks
Subscribe a URL to events, for every address or only for the addresses in subjects. Creating a subscription returns its signing secret once: store it.
| Event | Sent when |
|---|---|
| score.updated | The subject's Trust Score changed (a new snapshot in its score history). |
| risk.detected | A risk was detected for the subject. data carries the item with its classification. |
| rap_sheet.created | A RAP SHEET item was created for the subject. |
| wallet.cluster_changed | The wallet's funding cluster changed. A grouping by funding links, not a claim of common control. |
| aleph.assessment.updated | A new Aleph assessment. Always ALEPH_INFERENCE: an interpretation of the evidence, never a fact. |
| ping | Sent by POST /v1/webhooks/:id/test, to check your endpoint. |
Delivery
POST /your/webhook/endpoint
Content-Type: application/json
X-TrustLayer-Event: score.updated
X-TrustLayer-Delivery: whd_01JA…
X-TrustLayer-Signature: t=1791633600,v1=5f2b…c9e1
{
"id": "whe_01JA…",
"type": "score.updated",
"created_at": "2026-10-10T12:00:00.000Z",
"network": "robinhood",
"subject": "0x7F2c…A91b",
"data": { … }
}X-TrustLayer-Signatureist=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 with your secret over<t>.<raw body>. More than onev1may appear; accept the delivery when any one matches.- Verify against the raw body exactly as received, compare in constant time, and reject a timestamp more than 300 seconds from your clock. That stops forged and replayed deliveries.
- Answer with a 2xx status quickly and do slow work afterwards. Other answers and timeouts count as failures and are retried with backoff, so de-duplicate on the event
id. A subscription that keeps failing is disabled;GET /v1/webhooksshowsconsecutive_failuresanddisabled_reason. - Treat
dataas a notification and read the subject again for its current state. Analeph.assessment.updatedevent is an inference: route it to review, never treat it as a fact.
Verifying a delivery
import { verifyWebhook, WebhookVerificationError } from "@trust-layer/sdk";
// Next.js route handler (any Fetch-API runtime)
export async function POST(request: Request) {
const payload = await request.text(); // the raw body, unparsed
try {
const event = await verifyWebhook({
payload,
header: request.headers.get("x-trustlayer-signature"),
secret: process.env.TRUST_LAYER_WEBHOOK_SECRET!,
// toleranceSeconds: 300 (default)
});
await enqueue(event); // de-duplicate on event.id
} catch (error) {
if (error instanceof WebhookVerificationError) {
return new Response(null, { status: 400 });
}
throw error;
}
return new Response(null, { status: 204 });
}import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the exact bytes received. header: X-TrustLayer-Signature.
export function isAuthentic(rawBody: Buffer, header: string, secret: string) {
const fields = header.split(",").map((part) => part.trim().split("="));
const t = fields.find(([key]) => key === "t")?.[1];
if (!t || !/^\d+$/.test(t)) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.`)
.update(rawBody)
.digest();
return fields.some(([key, value]) =>
key === "v1" && /^[0-9a-f]{64}$/i.test(value ?? "") &&
timingSafeEqual(Buffer.from(value!, "hex"), expected));
}TypeScript SDK
@trust-layer/sdk has no runtime dependencies (fetch and Web Crypto) and runs on Node ≥ 20, Deno, Bun and edge runtimes. Every response is typed with the API's own field names. It retries 429s after Retry-After and 5xx or network errors with backoff (never repeating a create), validates addresses before sending, and returns a typed pending result for addresses still being indexed, or waits for them with waitForReady. baseUrl points it at any deployment.
# from this repository, until the first npm release
pnpm --filter @trust-layer/sdk build
# in a workspace package.json
"@trust-layer/sdk": "workspace:*"
# once published
npm install @trust-layer/sdkimport { TrustLayer, isPending } from "@trust-layer/sdk";
const tl = new TrustLayer({
apiKey: process.env.TRUST_LAYER_API_KEY, // server-side only
baseUrl: process.env.TRUST_LAYER_BASE_URL, // default https://api.trustlayer.xyz
});
const risk = await tl.risk("0x7F2c…A91b"); // RiskResponse | Pending
if (isPending(risk)) {
console.log("not rated yet:", risk.indexing?.status);
} else {
console.log(risk.trust_score, risk.band, risk.flags);
}
// Or wait until the address is indexed (polls, then returns data):
const ready = await tl.trust("0x7F2c…A91b", { waitForReady: { timeoutMs: 30_000 } });Methods
wallet, token, contract (the TRUST RECORD with a type check), trust, risk, trustHistory, rapSheet and record with page and item iterators, item, graph, aleph, alephHistory, tokenProfile, developer, search, batch, usage, webhooks.*, dispute, and verifyWebhook for receivers.
Decision policies
import { policy } from "@trust-layer/sdk";
const guard = policy({
minTrustScore: 400, // block below
warnBelowTrustScore: 550, // warn below
maxRiskLevel: "elevated", // block on high / severe
requireScored: false, // true: unscored and provisional block
treatPendingAs: "pending", // or "allow" | "warn" | "block"
blockOnFlagSeverity: "high", // VERIFIED / SIGNAL flags at or above block
aleph: { warnAt: "medium" }, // an inference warns; blockAt is opt-in
});
const { decision, reasons } = guard.evaluate(await tl.risk(recipient));
// decision: "allow" | "warn" | "block" | "pending"
// reasons[]: { code, effect, message, classification?, inference, evidence }- TRUST LAYER provides the evidence; your application makes the call. A policy turns a
/v1/riskanswer into allow, warn, block or pending, and every reason cites the score, the flag (with its class and event id) or the assessment behind it. - Only the score thresholds and VERIFIED or SIGNAL flags block by default.
- An ALEPH INFERENCE adds at most a warning, marked as inference. It blocks only when your policy sets
aleph.blockAt, and never changes the score. - An address that is still being indexed, or has too little history to score, is not evidence of risk: the reason says so.
Integration examples
Runnable TypeScript in packages/sdk/examples. They read TRUST_LAYER_BASE_URL and TRUST_LAYER_API_KEY from the environment.
Wallet: before signing
Checks the recipient before the user signs. Shows each flag with its class and the Aleph assessment apart from the score.
examples/wallet-presign-check.ts
Recipient Trust Score: 211 · HIGH RISK
Band high_risk · confidence 82% · score model v0.3
RAP SHEET (medium or worse):
HIGH Deterministic signal A token this wallet deployed lost…
Aleph inference (not a verified fact): high risk, confidence 62%
Decision: BLOCK (ask the user to confirm, or stop the transfer)Launchpad: before launching
Reads the deploying wallet with developer(): every earlier launch and how its own record reads.
examples/launchpad-deployer-check.ts
Deployer Trust Score: 834 · Strong
Historical Launches: 7 (5 tokens)
Launches with risk items (medium or worse): 0
High-Risk Events: 0AI agent: before sending funds
A payment guard. Several payees are screened with one batch call; anything still being indexed waits.
examples/agent-payment-policy.ts
const risk = await tl.risk(payee, { waitForReady: { timeoutMs: 20_000 } });
if (risk.trust_score !== null &&
risk.trust_score < PAYMENT_POLICY.minimumTrustScore) {
return reject();
}
const { decision, reasons } = guard.evaluate(risk);Webhook receiver
A node:http endpoint that verifies every delivery on the raw body, de-duplicates retries and handles each event type.
examples/webhook-receiver.ts
pnpm --filter @trust-layer/sdk example:webhooks --self-test
signed ping → HTTP 200
tampered ping → HTTP 400
unsigned ping → HTTP 400Conventions
- Addresses are accepted in lowercase or valid EIP-55 checksum form and returned checksummed.
- Errors always look like
{ "error": { "code": "…", "message": "…" } }. - Timestamps are ISO-8601 UTC. Token amounts and wei are decimal strings.
- Every item states its class (
VERIFIED,SIGNAL,ALEPH_INFERENCE) and the rule, catalog or model version that produced it. See the methodology. - Disagree with an item? File a dispute with
POST /v1/disputes(kindfalse_positive,incorrect_dataorother) and follow it atGET /v1/disputes/:id.