credgate-sdk

On-chain credit scoring for any DeFi lending protocol

Integrate wallet-based undercollateralized lending into any dApp. Analyze on-chain wallet history across Aave, DEXs, stablecoins and multiple chains — get a structured credit score, loan profile, and Merkle-verified proof on CreditCoin.

TypeScriptMerkle ProofsCreditCoinReact HooksMIT

Wallet Analysis

Aave history, stablecoin treasury, DEX activity, cross-chain maturity — one call

Credit Scores

0–100 score, tier, max loan size, recommended LTV, full breakdown by category

Merkle Proof Tracking

Real-time proof lifecycle: Sepolia attestation → CreditCoin USC → verified on-chain

React Ready

useCredGate + useSimpleScore hooks with polling, cooldown timer, and error states

Installation

Install via npm, yarn, or pnpm:

bash
npm install credgate-sdk
# or
yarn add credgate-sdk
# or
pnpm add credgate-sdk

Zero runtime dependencies — uses the native fetch API (Node 18+ and all modern browsers).

TypeScript project setup

Create a tsconfig.json in your project root. Do not use npx tsc --init — it enables verbatimModuleSyntax which conflicts with CommonJS and will cause import errors.

tsconfig.jsonjson
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Install ts-node for running scripts directly

bash
npm install --save-dev typescript ts-node @types/node

Environment variables

.envbash
CREDGATE_API_URL=http://localhost:3000   # your NestJS backend URL
CREDGATE_API_KEY=optional_key_here       # if you added auth to your backend

Quick Start

From install to first credit score. Below is a real script with the actual output from a live wallet:

test.tstypescript
import { CredGateClient, CredGateError, ErrorCode } from "credgate-sdk";

const client = new CredGateClient({
  apiUrl: "http://localhost:3000",  // your CredGate backend
});

async function main() {
  // Analyze + poll until score is ready
  const result = await client.analyzeWallet("0x05631891643A2E9dd5CC44293F14CAA4b4CD98B2", {
    timeout: 120_000,
  });

  console.log(result.score.creditScore);                   // 89
  console.log(result.score.tier);                          // "PRIME"
  console.log(result.score.loanProfile.maxLoanSizeUSD);    // 209998
  console.log(result.score.loanProfile.recommendedLTV);    // 70
  console.log(result.score.loanProfile.interestTier);      // "PRIME"
  console.log(result.score.riskLevel);                     // "LOW"
  console.log(result.score.riskScore);                     // 10
  console.log(result.score.scoreBreakdown);
  // { lending: 30, stable: 18.46, crossChain: 14, dex: 15, ageBonus: 13.85, riskPenalty: 2 }
  console.log(result.onchain.status);                      // "UPDATED"
}

main().catch(console.error);
bash
# Make sure your backend is running first
cd your-credgate-backend && npm run start:dev

# Then run the test
npx ts-node test.ts

Real terminal output

bash
🔍 Testing credgate-sdk...

1. Analyzing wallet...
✅ Score: 89
   Tier: PRIME
   Max Loan: $209998
   LTV: 70%
   Interest Tier: PRIME
   Risk: LOW (10)
   Breakdown: { lending: 30, stable: 18.46, crossChain: 14, dex: 15, ageBonus: 13.85, riskPenalty: 2 }
   On-chain status: UPDATED
   Proof status: not_found   # proof kicks off after RegistryWatcher detects ScoreUpdated

2. Getting cached score...
✅ Cached score: 89 | Tier: PRIME

3. Checking eligibility...
✅ Eligible to borrow

4. Getting max loan...
✅ Max loan: $209998

7. Polling proof status...
   → ⏳ Waiting for CreditCoin attestation — 14 blocks left (~168s)
   → ⏳ Waiting for CreditCoin attestation — 4 blocks left (~48s)
   → 📤 Submitting to CreditCoin USC
   → ✅ Proof verified on-chain!
      tx: 0xb996eb20aec01aa4b3e856b3ba0c6285ca668c2307d984f6f48246c4131d6b7d

CredGateClient

The main entry point. Create one instance and reuse it across your app.

typescript
import { CredGateClient } from "credgate-sdk";

const client = new CredGateClient({
  apiUrl: "http://localhost:3000",  // required — your NestJS backend
  apiKey: "sk_live_...",            // optional — forwarded as x-api-key header
  pollInterval: 3000,               // optional — ms between polls (default: 3000)
  timeout: 120_000,                 // optional — analysis timeout ms (default: 120000)
});
apiUrl*
string
Base URL of your CredGate NestJS backend. No trailing slash needed.
apiKey
string
Optional API key forwarded as x-api-key on every request.
pollInterval
number
How often to poll /wallet/result/:address in ms. Default: 3000.
timeout
number
Max ms before throwing ANALYSIS_TIMEOUT. Default: 120000.

analyzeWallet()

client.analyzeWallet(address, options?)ASYNC

Returns: Promise<AnalysisResult>

Triggers full wallet analysis — Aave lending history, stablecoin treasury, DEX activity, cross-chain maturity. Polls until the score is ready, then emits ScoreUpdated on Sepolia which kicks off the CreditCoin Merkle proof pipeline.

Parameters

address*
string
EVM wallet address (0x...). Checksummed or lowercase both work.
options.pollInterval
number
Override poll interval for this call only (ms).
options.timeout
number
Override timeout for this call only (ms).
options.waitForProof
boolean
Also wait for CreditCoin Merkle proof to reach success/failed before resolving.

Full usage

typescript
const result = await client.analyzeWallet("0x05631891643A2E9dd5CC44293F14CAA4b4CD98B2", {
  timeout: 120_000,
});

// Score
console.log(result.score.creditScore);   // 89
console.log(result.score.tier);          // "PRIME"
console.log(result.score.riskScore);     // 10
console.log(result.score.riskLevel);     // "LOW"

// Loan profile
console.log(result.score.loanProfile.maxLoanSizeUSD);  // 209998
console.log(result.score.loanProfile.recommendedLTV);  // 70
console.log(result.score.loanProfile.interestTier);    // "PRIME"

// Score breakdown
console.log(result.score.scoreBreakdown);
// { lending: 30, stable: 18.46, crossChain: 14, dex: 15, ageBonus: 13.85, riskPenalty: 2 }

// On-chain Sepolia status
console.log(result.onchain.status);  // "UPDATED"

Also wait for Merkle proof

typescript
const result = await client.analyzeWallet("0x...", {
  waitForProof: true,
  timeout: 1_800_000,  // 30 min — attestation takes 10–30 min
});

console.log(result.proof?.status);   // "success"
console.log(result.proof?.txHash);
// "0xb996eb20aec01aa4b3e856b3ba0c6285ca668c2307d984f6f48246c4131d6b7d"

Cooldown handling

Wallets have a cooldown in CreditScoreRegistry.sol (default 5 min). If active, an error is thrown:

typescript
import { CredGateError, ErrorCode } from "credgate-sdk";

try {
  await client.analyzeWallet(address);
} catch (err) {
  if (err instanceof CredGateError && err.code === ErrorCode.COOLDOWN_ACTIVE) {
    const secs = err.meta?.remainingSeconds as number;
    console.log(`Try again in ${Math.ceil(secs / 60)} minutes`);
  }
}

getScore()

client.getScore(address)

Returns: Promise<ScoreResult | null>

Fetches the latest cached score without triggering new analysis. Returns null if the wallet has never been analyzed. Fast — no polling involved.

typescript
const score = await client.getScore("0x...");

if (!score) {
  // Never analyzed — trigger it
  await client.analyzeWallet(address);
  return;
}

console.log(score.creditScore);                  // 89
console.log(score.tier);                         // "PRIME"
console.log(score.loanProfile.maxLoanSizeUSD);   // 209998
console.log(score.analyzedAt);                   // unix timestamp ms

// Common gating pattern
if (score.tier === "REJECT" || score.loanProfile.maxLoanSizeUSD === 0) {
  return showIneligibleState();
}
showBorrowUI(score.loanProfile.maxLoanSizeUSD);

Proof Tracking

After analysis, the RegistryWatcherService detects the ScoreUpdated event on Sepolia and starts the CreditCoin Merkle proof. Proof status starts as not_found until that happens.

getProofStatus()

client.getProofStatus(address)

Returns: Promise<ProofStatus>

Single snapshot of current proof state. Check blocksRemaining and estimatedWaitSeconds to show a progress indicator during waiting_attestation.

typescript
const proof = await client.getProofStatus("0x...");

console.log(proof.status);               // "waiting_attestation"
console.log(proof.blocksRemaining);      // 14
console.log(proof.estimatedWaitSeconds); // 168
console.log(proof.currentAttestedBlock); // 7450000
console.log(proof.targetBlock);          // 7450014

Poll with status labels

typescript
const PROOF_POLL_MS = 5000;

const poller = setInterval(async () => {
  const proof = await client.getProofStatus(address);

  const labels: Record<string, string> = {
    not_found:           "⏸  Not started yet",
    queued:              "📋 Queued",
    checking_contract:   "🔍 Checking contract",
    fetching_tx:         "📡 Fetching Sepolia tx",
    waiting_attestation: `⏳ ${proof.blocksRemaining} blocks left (~${proof.estimatedWaitSeconds}s)`,
    generating_proof:    "⚙️  Generating Merkle proof",
    submitting:          "📤 Submitting to CreditCoin USC",
    success:             `✅ Verified! tx: ${proof.txHash}`,
    failed:              `❌ Failed: ${proof.error}`,
  };

  console.log("→", labels[proof.status] ?? proof.status);

  if (proof.status === "success" || proof.status === "failed") {
    clearInterval(poller);
  }
}, PROOF_POLL_MS);

waitForProof()

client.waitForProof(address, options?)

Returns: Promise<ProofStatus>

Polls until success. Throws CredGateError with PROOF_FAILED on failure.

typescript
try {
  const proof = await client.waitForProof("0x...", {
    timeout: 1_800_000,   // 30 min
    pollInterval: 10_000,
  });
  console.log("CreditCoin tx:", proof.txHash);
  // "0xb996eb20aec01aa4b3e856b3ba0c6285ca668c2307d984f6f48246c4131d6b7d"
} catch (err) {
  if (err instanceof CredGateError && err.code === ErrorCode.PROOF_FAILED) {
    console.log("Proof failed:", err.message);
  }
}

On-chain Status

🔗 Contract deployment: Only CreditScoreRegistry.sol is on Sepolia. All other contracts (CreditScoreUSC, CreditAggregator, CreditVault) are deployed on CreditCoin USC Testnet — chain ID 102036, RPC https://rpc.usc-testnet2.creditcoin.network.

The getOnChainStatus() call reflects the Sepolia CreditScoreRegistry state. The Merkle proof (tracked separately) finalises the score on CreditCoin USC.

typescript
const status = await client.getOnChainStatus("0x...");

// "UPDATED"         = score stored on Sepolia CreditScoreRegistry ✓
// "COOLDOWN_ACTIVE" = can't update yet (5 min default cooldown in contract)
// "NOT_SUBMITTED"   = never pushed on-chain for this wallet

console.log(status.status);            // "UPDATED"
console.log(status.txHash);            // Sepolia tx of ScoreUpdated event
console.log(status.reportHash);        // keccak256 of score data
console.log(status.remainingSeconds);  // seconds until next update allowed
💡 Make sure the watcher is running. The RegistryWatcherService lines in main.ts are commented out by default. Uncomment them to enable automatic proof generation whenever a ScoreUpdated event is emitted.

Helpers

isEligible()

Returns false if tier is REJECT or maxLoanSizeUSD is 0. Great for server-side gating.

typescript
const eligible = await client.isEligible("0x...");

// Server-side loan gating (Express / NestJS)
if (!(await client.isEligible(userAddress))) {
  return res.status(403).json({ error: "Insufficient credit score" });
}
// proceed with disbursement...

getMaxLoan()

typescript
const maxLoan = await client.getMaxLoan("0x...");   // 209998

if (requestedAmount > maxLoan) {
  throw new Error(`Exceeds credit line of $${maxLoan}`);
}

All methods at a glance

typescript
client.analyzeWallet(address, options?)   // trigger analysis → AnalysisResult
client.getScore(address)                  // cached score → ScoreResult | null
client.getProofStatus(address)            // Merkle proof state → ProofStatus
client.waitForProof(address, options?)    // poll until done → ProofStatus
client.getOnChainStatus(address)          // Sepolia registry → OnChainStatus
client.isEligible(address)                // quick gate → boolean
client.getMaxLoan(address)                // max loan USD → number

React Hooks

Import from credgate-sdk/react — separate entry point, fully tree-shakeable, no extra install.

useCredGate — full hook

typescript
import { CredGateClient } from "credgate-sdk";
import { useCredGate } from "credgate-sdk/react";
import { useAccount } from "wagmi";

// Create once outside the component (or in a Context)
const client = new CredGateClient({ apiUrl: "http://localhost:3000" });

export function CreditWidget() {
  const { address } = useAccount();
  const {
    score,              // ScoreResult | null
    proof,              // ProofStatus | null
    onchain,            // OnChainStatus | null
    loading,            // true while fetching cached data on mount
    analyzing,          // true while analysis is in progress
    error,              // string | null
    cooldownRemaining,  // number — counts down from remainingSeconds
    analyze,            // () => Promise<void> — trigger new analysis
    refetch,            // () => Promise<void> — reload cached data
  } = useCredGate(client, address);

  if (loading) return <p>Loading...</p>;

  return (
    <div>
      <h2>{score?.creditScore ?? "—"} / 100</h2>
      <p>Tier: {score?.tier ?? "Not analyzed"}</p>
      <p>Max Loan: ${score?.loanProfile.maxLoanSizeUSD ?? 0}</p>
      <p>LTV: {score?.loanProfile.recommendedLTV ?? 0}%</p>

      {proof?.status === "waiting_attestation" && (
        <p>⏳ {proof.blocksRemaining} blocks left(~{proof.estimatedWaitSeconds}s)</p>
      )}
      {proof?.status === "success" && <p>✅ Verified on CreditCoin</p>}

      <button onClick={analyze} disabled={analyzing || cooldownRemaining > 0}>
        {analyzing ? "Analyzing..."
          : cooldownRemaining > 0 ? `Cooldown: ${cooldownRemaining}s`
          : score ? "Re-analyze" : "Check Score"}
      </button>

      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}

useCredGate options

autoAnalyze
boolean
Trigger analysis on mount if no cached score exists. Default: false.
proofPollInterval
number
Poll proof every N ms. 0 = disabled. Default: 5000.

useSimpleScore — minimal hook

typescript
import { useSimpleScore } from "credgate-sdk/react";

export function LoanGate({ children }: { children: React.ReactNode }) {
  const { address } = useAccount();
  const { eligible, tier, maxLoan, recommendedLTV, loading, analyze } = useSimpleScore(client, address);

  if (loading) return <Spinner />;
  if (!eligible) return (
    <div>
      <p>You need a credit score to borrow.</p>
      <button onClick={analyze}>Check Eligibility</button>
    </div>
  );

  return (
    <div>
      <p>✅ {tier} · Up to ${maxLoan} at {recommendedLTV}% LTV</p>
      {children}
    </div>
  );
}

Error Handling

All SDK methods throw typed CredGateError with a code field:

typescript
import { CredGateClient, CredGateError, ErrorCode } from "credgate-sdk";

try {
  await client.analyzeWallet(address);
} catch (err) {
  if (!(err instanceof CredGateError)) throw err;

  switch (err.code) {
    case ErrorCode.COOLDOWN_ACTIVE:
      const secs = err.meta?.remainingSeconds as number;
      console.log(`Try again in ${Math.ceil(secs / 60)} minutes`);
      break;
    case ErrorCode.ANALYSIS_TIMEOUT:
      await client.analyzeWallet(address); // retry
      break;
    case ErrorCode.WALLET_NOT_FOUND:
      console.log("Wallet has no data");
      break;
    case ErrorCode.PROOF_FAILED:
      console.log("CreditCoin proof failed:", err.meta?.txHash);
      break;
    case ErrorCode.UNAUTHORIZED:
      console.log("Check your API key");
      break;
    case ErrorCode.NETWORK_ERROR:
      console.log("Backend unreachable:", err.message);
      break;
  }
}
COOLDOWN_ACTIVEContract cooldown active. err.meta.remainingSeconds available.
ANALYSIS_TIMEOUTBackend exceeded timeout. Safe to retry.
WALLET_NOT_FOUNDNo cached result. Call analyzeWallet() first.
PROOF_FAILEDCreditCoin proof failed. Check err.meta.txHash.
UNAUTHORIZEDMissing or invalid API key (401/403).
NETWORK_ERRORfetch() failed — backend unreachable.
UNKNOWNUnhandled HTTP error. Status in err.message.

TypeScript Types

typescript
import type {
  CredGateConfig,      // client constructor config
  AnalyzeOptions,      // analyzeWallet() options
  AnalysisResult,      // returned by analyzeWallet()
  ScoreResult,         // score + tier + loanProfile + breakdown
  LoanProfile,         // { recommendedLTV, interestTier, maxLoanSizeUSD }
  ScoreBreakdown,      // { lending, stable, crossChain, dex, ageBonus, riskPenalty }
  ProofStatus,         // Merkle proof state + progress fields
  ProofStatusValue,    // union of all proof status strings
  OnChainStatus,       // Sepolia registry state
  CreditTier,          // "ELITE"|"PRIME"|"PREFERRED"|"STANDARD"|"REJECT"
  RiskLevel,           // "LOW"|"MEDIUM"|"HIGH"
} from "credgate-sdk";

ScoreResult shape

typescript
interface ScoreResult {
  address: string;
  creditScore: number;       // 0–100             (e.g. 89)
  tier: CreditTier;          //                   (e.g. "PRIME")
  riskScore: number;         // 0–100             (e.g. 10)
  riskLevel: RiskLevel;      //                   (e.g. "LOW")
  loanProfile: {
    recommendedLTV: number;  // 0–70              (e.g. 70)
    interestTier: string;    //                   (e.g. "PRIME")
    maxLoanSizeUSD: number;  // capital × LTV     (e.g. 209998)
  };
  scoreBreakdown: {
    lending: number;         // Aave (max 30)     → 30
    stable: number;          // Stablecoin (max 35) → 18.46
    crossChain: number;      // Multi-chain (max 20) → 14
    dex: number;             // DEX (max 15)      → 15
    ageBonus: number;        // Age log-scale     → 13.85
    riskPenalty: number;     // Deducted          → 2
  };
  analyzedAt: number;        // unix ms
}

ProofStatus shape

typescript
interface ProofStatus {
  status: ProofStatusValue;           // current lifecycle stage
  jobId?: string;                     // "job_0xabc123_1709..."
  txHash?: string;                    // CreditCoin USC tx on success
  error?: string;                     // error detail on failed
  currentAttestedBlock?: number;      // latest attested Sepolia block on CreditCoin
  targetBlock?: number;               // Sepolia block your ScoreUpdated tx is in
  blocksRemaining?: number;           // targetBlock - currentAttestedBlock
  estimatedWaitSeconds?: number;      // blocksRemaining × 12s
}

Credit Tiers

Derived from credit score — determines LTV, interest, and max loan size:

TierScoreLTVInterestNotes
PRIME≥ 7570%LowStrong history (like the test wallet above at 89)
PREFERRED≥ 6060%StandardSolid on-chain track record
STANDARD≥ 4550%HigherLimited history or moderate risk
HIGH_RISK≥ 3035%HighPoor metrics — small credit line
REJECT< 300%N/ANo credit line issued
⚠️ Note: A high tier doesn't guarantee a large loan. Max loan is capped by capital base (stablecoin inflow × retention ratio). A PRIME wallet with no stablecoin history may still get a small credit line.

Proof Lifecycle

After analyzeWallet() completes, the RegistryWatcherService detects ScoreUpdated on Sepolia and starts the pipeline. This is what the real terminal output looked like step by step:

not_foundInitial state — watcher hasn't detected ScoreUpdated yet
queuedJob created, about to start
checking_contractVerifying CreditScoreUSC has aggregator + authorized source set
fetching_txFetching the Sepolia tx from RPC
waiting_attestationPolling 0xFD3 precompile — waiting for Sepolia block to be attested on CreditCoin (10–30 min, blocksRemaining available)
generating_proofCalling proof-gen API to build the Merkle Merkle proof
submittingCalling CreditScoreUSC.submitScoreFromQuery() on CreditCoin USC Testnet (chain ID 102036)
successScore verified and stored in CreditAggregator ✓ — txHash available
failedProof submission failed — re-analyze to retry
💡 waiting_attestation is the longest stage. The SDK exposes blocksRemaining and estimatedWaitSeconds so you can show a live progress bar. In the real test above: 14 blocks (~168s) → 4 blocks (~48s) → submitting → success.
credgate-sdk v1.0.6 · MIT License