Base URL https://soci4l.net/api/v1.
Authentication
Every endpoint except GET /api/v1 requires an API key, passed in the X-Api-Key header. Keys look like s4_live_… and are scoped to your wallet.
curl https://soci4l.net/api/v1/score/0x8ab0...e1b1 \ -H "X-Api-Key: s4_live_..."
Create and revoke keys yourself: connect your wallet, open the Developer tab in your dashboard, and create a key (a one-time wallet signature confirms ownership). The raw key is shown once. Store it securely; it's held only as a hash and cannot be recovered. Keep keys server-side; never ship them in client code.
Plans and rate limits
| Plan | Requests / 30 days | Includes | Price |
|---|---|---|---|
| Free | 2,500 | Single-address scoring | Free |
| Builder | 25,000 | + batch screening, + 90-day history | 49.00 USDC |
| Growth | 250,000 | + EIP-712 signed attestations | 249.00 USDC |
| Scale | 1,500,000 | Everything, at volume | 999.00 USDC |
Paid in USDC on Avalanche C-Chain, per 30 days, no auto-renewal. Batch calls consume one unit per address; a 50-address call costs 50. Calling an endpoint outside your plan returns 402 with PLAN_REQUIRED.
Quotas reset on a rolling 30-day period. Batch calls consume one unit per address; a 50-address call costs 50. Every response carries:
X-RateLimit-Limit: your ceiling for the periodX-RateLimit-Remaining: requests left this periodX-RateLimit-Reset: ISO 8601 reset time
Calling an endpoint your plan does not include returns 402 with { code: "PLAN_REQUIRED", requiredPlan, upgrade }. Nothing is deducted from your quota when that happens.
On top of the daily quota there is a per-key burst brake: 60 requests/min on single-address endpoints and 10 requests/min on batch. A burst 429 carries a Retry-After header (seconds); back off and retry.
Get a wallet score
/api/v1/score/:addressThe full reputation read for one address. On-chain signals are read live (24-hour cache); profile/social signals are first-party. Works for any C-Chain address whether or not it has a SOCI4L profile.
Response
{
"address": "0x8ab0cf264df99d83525e9e11c7e4db01558ae1b1",
"headline": {
"score": 60, // 0 to 100 composite, the public headline
"tier": "elite",
"tierLabel": "Elite",
"model": "s4-composite"
},
"s4": {
"humanity": 72.1, // real-person evidence (0 to 100)
"activity": 55.3, // on-chain activity, recency-decayed
"social": 38.0, // follower graph, anomaly-damped
"economic": 61.4, // costly on-chain spend (log scale)
"composite": 60.2, // weighted blend → headline.score
"confidence": 0.75 // data coverage (0 to 1); low data ≠ low score
},
"graph": {
"analyzed": true,
"flags": [], // e.g. ["follow_ring","thin_followers"]
"socialDamping": 1 // 0.25 to 1 multiplier applied to "social"
},
"explanation": {
"summary": "Scores 60 on long history and 3 other positive signals.",
"confidence": {
"value": 0.75, "band": "high", "label": "High",
"detail": "Confidence measures how much of this wallet we can actually see. ..."
},
"reasons": [ // ranked: coverage caveats, then risks, then positives
{
"code": "LONG_HISTORY", // closed set; branch on this
"kind": "positive", // "positive" | "risk" | "coverage"
"dimension": "humanity",
"title": "Long history",
"detail": "This wallet has a long on-chain history, ..."
}
]
},
"verifiedHuman": 2, // permanent Verified Human #N, or null
// ── legacy v1 (kept for back-compat; prefer headline + s4) ──
"score": 43.4,
"tier": "established",
"breakdown": { "walletAge": 15, "txActivity": 10, "gasSpent": 8, "...": 0 },
"fetchedAt": "2026-06-24T10:30:00.000Z",
"model": "v2-composite-headline+v1-legacy"
}explanation is the auditable half of the read: ranked reasons in plain English, safe to show the wallet you are screening. Branch on code (a closed set) and show detail verbatim. Reasons state direction and category only, the thresholds behind them are never published, because a published threshold is a manual for gaming the score. confidence is data coverage, not suspicion: a thin read means we cannot see much of the wallet yet.
Batch scoring
/api/v1/score/batchScore 1 to 50 addresses in one call: airdrop filtering, allowlist checks, sybil screening. Signals are read cache-only for speed: a result with signals: "none" has never been scored; warm it once via the single-address endpoint. Each address consumes one rate-limit unit.
Request
curl -X POST https://soci4l.net/api/v1/score/batch \
-H "X-Api-Key: s4_live_..." \
-H "Content-Type: application/json" \
-d '{ "addresses": ["0x8ab0...e1b1", "0xd8da...6045"] }'Response
{
"count": 2,
"results": [
{
"address": "0x8ab0...e1b1",
"score": 43.4, "tier": "established",
"s4": { "humanity": 72.1, "composite": 60.2, "confidence": 0.75, "...": 0 },
"graph": { "analyzed": true, "flags": [], "socialDamping": 1 },
"verifiedHuman": 2,
"signals": "fresh" // "fresh" | "stale" | "none"
}
// ...
],
"fetchedAt": "2026-06-24T10:30:00.000Z"
}Score history
/api/v1/score/:address/historyDaily snapshots for trend analysis. Query params: days (1–90, default 30) and include=breakdown to add per-signal points to each entry. The time series itself is an anti-sybil signal; sudden discontinuities are suspicious.
Response
{
"address": "0x8ab0...e1b1",
"days": 30,
"count": 28,
"history": [
{ "date": "2026-05-28", "score": 41.0, "tier": "established" },
{ "date": "2026-05-29", "score": 41.4, "tier": "established" }
// ...
],
"fetchedAt": "2026-06-24T10:30:00.000Z"
}Signed attestation
/api/v1/score/:address/signedThe score as an EIP-712 signed attestation: the canonical struct, the domain and type definitions, and a service signature over them. Verify it offline with viem or ethers verifyTypedData, or paste it into the verify page to check it in the browser. Compare the recovered signer against the attestation.signer field of GET /api/v1. Returns 501 on deployments where signing is not enabled. Consumes one rate-limit unit.
Response
{
"domain": {
"name": "SOCI4L Attestation", "version": "1",
"chainId": 43114, "verifyingContract": "0x873e...1A13"
},
"types": { "ScoreAttestation": [ /* EIP-712 field definitions */ ] },
"primaryType": "ScoreAttestation",
"message": {
"subject": "0x8ab0...e1b1",
"score": "60", // uint256 fields are decimal strings
"humanity": "72",
"tier": 4, // 0 starter ... 5 legendary
"verifiedHuman": true,
"issuedAt": "1783937400",
"expiresAt": "1784542200", // default TTL: 7 days
"nonce": "1783937400123"
},
"signature": "0x...", // 65-byte service signature
"signer": "0x...", // must match attestation.signer at GET /api/v1
"readable": { /* same fields, human-readable */ },
"model": "s4-composite"
}Health
/api/v1/healthPublic uptime probe, no API key required. Returns HTTP 200 when the app and its database are reachable, 503 when degraded. Point your monitor at the status code.
Response
{
"status": "ok", // "ok" | "degraded" (degraded returns HTTP 503)
"version": "1",
"db": "ok", // "ok" | "unreachable"
"time": "2026-07-12T10:30:00.000Z"
}Response schema
The headline (composite 0 to 100 + tier) is the number to show users. The s4 vector is what you threshold on for B2B logic. Legacy score/tier are the original v1 fields, kept for back-compat.
S4 dimensions (0 to 100)
humanityIs there a real person behind this wallet? Verified socials + wallet age.activityOn-chain activity, recency-decayed. Volume is log-damped.socialFollower-graph standing, damped by anomaly heuristics.economicCostly, timestamped spend: gas, premium, paid slug, donations (log scale).compositeWeighted blend of the four; this is headline.score.confidence0 to 1 data coverage. Low data lowers confidence, not the score.Tier bands (composite)
Graph anomaly flags
follow_ringMost followers are followed right back, a reciprocal ring.follower_burstMost followers arrived inside a single 24-hour window.thin_followersFollowers are mostly unclaimed wallets with no on-chain history.self_donationDonations sent from the address to itself.Flags shrink socialDamping (0.25 to 1) and lower the vector's confidence. They never silently zero a score.
Errors
400Invalid address, or a malformed batch body / out-of-range params.401Missing or invalid API key.429Rate limit exceeded. Honor the Retry-After header (burst) or wait for X-RateLimit-Reset (daily quota).500Scoring failed server-side. Safe to retry with backoff.501Signed attestations not enabled on this deployment (signed endpoint only).Errors return { "error": "message" }. A 400 from the batch endpoint also includes an invalid array listing the offending addresses.
Code examples
JavaScript
const res = await fetch(
"https://soci4l.net/api/v1/score/" + address,
{ headers: { "X-Api-Key": process.env.SOCI4L_API_KEY } }
);
if (res.status === 429) throw new Error("rate limited");
const data = await res.json();
// Gate a mint on real on-chain humanity
if (data.s4.humanity >= 70 && data.s4.confidence >= 0.5) {
allowMint(address);
}Python
import os, requests
r = requests.get(
f"https://soci4l.net/api/v1/score/{address}",
headers={"X-Api-Key": os.environ["SOCI4L_API_KEY"]},
)
r.raise_for_status()
data = r.json()
print(data["headline"]["score"], data["headline"]["tier"])TypeScript SDK
soci4l-sdk wraps every endpoint above with typed responses, retries that honour Retry-After, and typed errors. Zero runtime dependencies, ESM + CommonJS, Node 18+. Install from the repository for now: the npm release is still pending.
npm install github:SOCI4LNET/SOCI4L-SDK
Gate one address
import { Soci4lClient } from 'soci4l-sdk'
const client = new Soci4lClient({ apiKey: process.env.SOCI4L_API_KEY! })
const { passed, s4, failures } = await client.verify(address, {
humanity: 50, // S4 humanity, 0 to 100
confidence: 0.5, // data coverage, don't gate on a thin read
noGraphFlags: true, // no follow-graph anomalies
})
if (!passed) {
// Every miss carries a reason you can show the person you rejected.
console.log(failures.map((f) => f.reason))
}Every threshold is optional: humanity, activity, social, economic, composite (alias score), confidence, verifiedHuman, noGraphFlags.
Screen a list
// Any length: deduped, chunked to 50, sent sequentially so a
// long list never trips the batch burst limit.
const verdicts = await client.verifyMany(addresses, { humanity: 40 })
const rejected = verdicts.filter((v) => !v.passed)
// Addresses SOCI4L has never scored fail with an explicit 'signals'
// reason instead of quietly passing. Warm them, then re-run.
const cold = rejected.filter((v) => v.failures.some((f) => f.check === 'signals'))
for (const v of cold) await client.getScore(v.address)Verify a signed score
import { typedDataFor, verifyAttestation } from 'soci4l-sdk'
const signed = await client.getSignedScore(address)
// Verify it yourself with viem/ethers…
const payload = typedDataFor(signed) // { domain, types, primaryType, message }
// …or let the SDK recover the signer, pinned to the published anchor.
const { valid, problems } = await verifyAttestation(signed, {
expectedSigner: (await client.describe()).attestation.signer!,
})verifyAttestation needs the optional peer dependency viem; typedDataFor works with any EIP-712 verifier. Anyone can also check a payload by hand at /verify.
Rate-limit headers from the last response are on client.lastRateLimit. Failures are typed: Soci4lAuthError, Soci4lRequestError, Soci4lRateLimitError (carries retryAfterSeconds), Soci4lNotEnabledError, Soci4lApiError, Soci4lNetworkError, Soci4lTimeoutError.
Common use cases
- Airdrop filtering: batch-score the claim list, drop low
humanity+ flagged graphs. - Mint / allowlist gating: require
humanity ≥ Nandconfidence ≥ 0.5. - Governance weighting: weight votes by
compositeoreconomic. - Sybil screening: flag clusters via the graph-anomaly
flagsand history discontinuities.