SOCI4L

API Reference

A reputation read for any Avalanche C-Chain address: a 0-100 composite headline, the four-dimension S4 vector, follow-graph anomaly flags, and a Verified Human number. REST, JSON, one header to authenticate.

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

PlanRequests / 30 daysIncludesPrice
Free2,500Single-address scoringFree
Builder25,000+ batch screening, + 90-day history49.00 USDC
Growth250,000+ EIP-712 signed attestations249.00 USDC
Scale1,500,000Everything, at volume999.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.

Create a key or upgrade a plan

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 period
  • X-RateLimit-Remaining: requests left this period
  • X-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

GET/api/v1/score/:address

The 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

POST/api/v1/score/batch

Score 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

GET/api/v1/score/:address/history

Daily 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

GET/api/v1/score/:address/signed

The 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

GET/api/v1/health

Public 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)

Legendary80 to 100
Elite60 to 79
Established40 to 59
Rising20 to 39
Newcomer10 to 19
Starter0 to 9

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 ≥ N and confidence ≥ 0.5.
  • Governance weighting: weight votes by composite or economic.
  • Sybil screening: flag clusters via the graph-anomaly flags and history discontinuities.