Developers

Integrate StellarCred

How it works

StellarCred stores zero-knowledge proofs on Stellar. Your protocol reads them with one contract call. No API keys, no backend, no data handling — the only thing you trust is the on-chain ProofRegistry.

Installation

npm install @stellarcred/sdk

Checking a claim

The primary call. Returns true if the wallet has a valid, unexpired proof of the claim. For parameterised claims (age, income, funds), pass minThreshold to enforce the threshold on-chain — trustlessly.

import { StellarCred } from "@stellarcred/sdk";

// Binary claim (kyc, jurisdiction) — no threshold
const kycOk = await StellarCred.hasClaim(wallet, "kyc");

// Age gate — proof must have been generated with threshold_years >= 21
const ageOk = await StellarCred.hasClaim(wallet, "age", { minThreshold: 21 });

// Funds gate — proof must certify balance >= $50,000
const fundsOk = await StellarCred.hasClaim(wallet, "funds", { minThreshold: 50000 });

Fetching all claims

Protocols that gate on multiple claims simultaneously benefit from fetching everything at once rather than making N separate hasClaim calls.

import { StellarCred } from "@stellarcred/sdk";

const claims = await StellarCred.getClaims(wallet);
// {
//   kyc:          { verified: true,  expiry: 1780000000 },
//   age:          { verified: true,  threshold: 21, expiry: 1780000000 },
//   income:       { verified: false },
//   jurisdiction: { verified: true,  expiry: 1780000000 },
//   funds:        { verified: false },
// }

// Gate on multiple claims at once
const canAccess = claims.kyc.verified && claims.age.verified;

Configuration

Call configure() once at startup, or set env vars. Both approaches work in Node.js, Next.js, and edge runtimes.

import { StellarCred } from "@stellarcred/sdk";

// Option A — explicit (recommended for servers / edge)
StellarCred.configure({
  registryId: process.env.PROOF_REGISTRY_ID,
  rpcUrl: "https://soroban-testnet.stellar.org",
  networkPassphrase: "Test SDF Network ; September 2015",
});

// Option B — env vars (auto-read at import time)
// STELLARCRED_REGISTRY_ID=C...
// STELLARCRED_RPC_URL=https://soroban-testnet.stellar.org
// (also reads NEXT_PUBLIC_PROOF_REGISTRY_ID / NEXT_PUBLIC_RPC_URL)

A missing registryId doesn’t throw — it makes every hasClaim/getClaims call silently return false/[], which can look like “nobody is verified” instead of “misconfigured.” Use healthCheck() to diagnose it directly (a dev-only console warning also fires automatically the first time this happens).

const health = StellarCred.healthCheck();
// { configured: false, registryId: false, rpcUrl: true, networkPassphrase: true,
//   missing: ["registryId"] }
if (!health.configured) console.error("StellarCred misconfigured:", health.missing);

// Or just: StellarCred.isConfigured() // boolean

Redirecting users to verify

If a user hasn’t verified yet, send them to StellarCred and get them back automatically. Use claimParams to customise thresholds.

import { StellarCred } from "@stellarcred/sdk";

// KYC gate — basic redirect
const kycUrl = StellarCred.buildVerifyUrl({
  returnUrl: 'https://yourapp.xyz/deposit',
  claim: 'kyc',
});

// Age gate — require 21+
const ageUrl = StellarCred.buildVerifyUrl({
  returnUrl: 'https://yourapp.xyz/markets',
  claim: 'age',
  claimParams: { threshold_years: '21' },
});

// Funds gate — require balance ≥ $50,000
const fundsUrl = StellarCred.buildVerifyUrl({
  returnUrl: 'https://yourapp.xyz/vault',
  claim: 'funds',
  claimParams: { threshold: '50000' },
});

// The return URL includes sc_verified=true, sc_wallet=<address>, and sc_claims=<types>
// sc_claims contains only the claim types issued in the current session.
const verified = await StellarCred.hasClaim(wallet, "kyc");

The return URL params are untrusted hints, not a proof. Nothing binds this redirect to your session — anyone can craft a URL shaped exactly like a real one and open it. Use parseReturnParams to read them, but always re-verify with hasClaim or verifyWalletClaim against the on-chain ProofRegistry before granting access.

⚠️ Wallet spoofing pitfall: Calling hasClaim(wallet) alone on your backend only proves that someone owns credentials for that address — not that the current caller actually controls that private key. For secure access gating, have the user sign a challenge and call verifyWalletClaim.

import { StellarCred, createWalletChallenge, verifyWalletClaim } from "@stellarcred/sdk";

// 1. Issue a replay-protected challenge from your backend:
const challenge = createWalletChallenge({
  domain: "yourapp.xyz",
  statement: "Sign in to access gated features",
});

// 2. Client signs challenge.message with their wallet (e.g. Freighter)

// 3. Verify BOTH wallet ownership and on-chain credential in one call:
const result = await verifyWalletClaim({
  wallet: clientWallet,
  challenge,
  signature: clientSignature,
  claim: "kyc",
});

if (!result.ok) {
  // Rejected: invalid signature, expired/replayed challenge, or missing credential
  throw new Error(result.error);
}
// Caller proved control of wallet AND holds valid on-chain KYC proof!

Canonical Integration Example

Looking for a complete, runnable reference application demonstrating the full integration pattern end to end? See examples/canonical-integration in the repository.

It includes interactive wallet challenge verification (eliminating the wallet-spoofing pitfall), session token generation, server-side route gating, and granular failure-state handling (unverified, expired, revoked, wrong issuer, unmet threshold, replayed nonce).

Available claim types

ClaimWhat it provesIssued by
kycIdentity verifiedKYC provider
ageAge ≥ 18 (threshold configurable)KYC provider
incomeIncome ≥ thresholdFinancial data provider
jurisdictionCountry not restrictedKYC provider
fundsBalance ≥ thresholdPlaid / bank attestation
accreditationNet worth ≥ thresholdFinancial institution

Contract addresses

The deployed StellarCred contracts on testnet.

See DEPLOYMENTS.md for the authoritative contract IDs, versions, WASM hashes, and deployment dates for each network.

NEXT_PUBLIC_ISSUER_REGISTRY_ID
CD4WNFA5SVOCZVEBXG3BNODBWNQMJRVBT6YH4J5M2FWHQ2SWMI7XWETS
NEXT_PUBLIC_CREDENTIAL_VERIFIER_ID
CDDSADPJRYMNLX2DTSN5OQO3XDPPIVQMU5WAPY2PWHKHAQZMMP5VK2CF
NEXT_PUBLIC_PROOF_REGISTRY_ID
CCB4LNZVHHBM34UNHKD2D57BENKWM27KTDNEQWYYFRPIGDNYI77QYIVO
NEXT_PUBLIC_GATED_POOL_ID
CC2CENI7KIKPF6TAGPORUVKGNADLP6MKIGYVSTGYOAZETOXECQ56X42O

Calling the contract directly

Prefer Soroban? Call ProofRegistry from your own contract — no SDK required. Use is_verified for binary claims and check_claim for threshold enforcement. Both take a trailing trusted_issuers parameter — pass None to accept a proof from any registered issuer (unchanged default), or Some(vec![...]) to restrict a claim to specific issuers, e.g. accepting KYC only from Persona or Jumio.

// Binary claim (kyc, jurisdiction) — any registered issuer accepted
let registry = ProofRegistryClient::new(&env, &registry_id);
let (verified, _, _) = registry.is_verified(&holder, &symbol_short!("kyc"), &None);
require!(verified, Error::KycRequired);

// Parameterised claim — enforce minimum threshold on-chain
let eligible = registry.check_claim(&holder, &symbol_short!("funds"), &Some(50_000u64), &None);
require!(eligible, Error::InsufficientFunds);

// Restrict which issuer(s) a claim must come from
let trusted = vec![&env, persona_issuer.clone(), jumio_issuer.clone()];
let kyc_ok = registry.check_claim(&holder, &symbol_short!("kyc"), &None, &Some(trusted));
require!(kyc_ok, Error::KycRequired);
Loading playground…