End-to-End Credential Flow
This guide walks through the complete lifecycle of a Verifiable Credential on Solidus: from creating an identity, through KYC verification and credential issuance, to presentation, verification, and revocation. Each step includes code and an explanation of what happens on-chain versus off-chain.
Overview
Create DID ──> Complete KYC ──> Receive Credential ──> Store
│
Revoke <── Verify <── Present1. Create a DID
Every credential holder needs a Decentralized Identifier. Creating a DID generates an Ed25519 key pair and registers the public key on the Solidus blockchain.
import * as ed25519 from '@noble/ed25519'
import { createSdk } from '@solidus-network/sdk'
// Generate a key pair
const privateKey = ed25519.utils.randomPrivateKey()
const publicKey = await ed25519.getPublicKeyAsync(privateKey)
const privateKeyHex = Buffer.from(privateKey).toString('hex')
const publicKeyHex = Buffer.from(publicKey).toString('hex')
// Create the SDK instance
const sdk = createSdk({
mode: 'testnet',
chain: {
rpcUrl: 'https://rpc.solidus.network',
signerPrivateKey: privateKeyHex,
network: 'testnet',
},
})
// Register the DID on-chain
const did = await sdk.did.create({
publicKey: publicKeyHex,
keyType: 'Ed25519VerificationKey2020',
})
console.log('DID:', did.id)
// => did:solidus:testnet:7Hk3mRtQZv...On-chain: A DID Document is written to the Solidus DID Registry. The document contains the public key and verification methods. The DID address is derived from a BLAKE3 hash of the public key.
Off-chain: The private key stays on the user’s device. It is never sent to the network.
2. Complete KYC Verification
The user completes identity verification through the Solidus Verify service. This can be initiated programmatically or by redirecting the user to the hosted verification page.
// Create a verification session via the Verify API
const session = await fetch('https://verify.solidus.network/v1/verifications', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
subjectDid: did.id,
level: 1,
redirectUrl: 'https://myapp.com/verification-complete',
webhook: {
url: 'https://myapp.com/webhooks/verify',
events: ['verification.completed', 'verification.failed'],
},
}),
})
const { id: sessionId, sessionUrl } = await session.json()
// Redirect the user to the verification page
// sessionUrl => https://verify.solidus.network/session/abc123On-chain: Nothing happens yet. KYC verification is an off-chain process.
Off-chain: The user uploads identity documents (passport, ID card, or driver’s license), completes a liveness check, and the Verify service runs OCR, face matching, and document authenticity checks.
3. Receive the Credential
After successful verification, the Verify service issues a Verifiable Credential to the user’s DID. The credential is signed by the Solidus Verify issuer DID and its hash is anchored on-chain.
// After KYC passes, query the issued credential
const verification = await fetch(
\`https://verify.solidus.network/v1/verifications/${sessionId}\`,
{
headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
}
)
const result = await verification.json()
// The credential object
const credential = result.credential
console.log('Credential ID:', credential.id)
console.log('Type:', credential.type)
// => ["VerifiableCredential", "KYCCredential"]
console.log('Issuer:', credential.issuer)
// => did:solidus:testnet:verify-service
console.log('Claims:', credential.credentialSubject)
// => { id: "did:solidus:testnet:7Hk3...", level: 1, country: "US" }The credential follows the W3C Verifiable Credentials Data Model. Its structure looks like this:
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://solidus.network/credentials/v1"
],
"id": "urn:uuid:credential-id-here",
"type": ["VerifiableCredential", "KYCCredential"],
"issuer": "did:solidus:testnet:verify-service",
"issuanceDate": "2026-05-07T12:00:00Z",
"expirationDate": "2027-05-07T12:00:00Z",
"credentialSubject": {
"id": "did:solidus:testnet:7Hk3mRtQZv...",
"level": 1,
"country": "US",
"verifiedAt": "2026-05-07T12:00:00Z"
},
"proof": {
"type": "Ed25519Signature2020",
"created": "2026-05-07T12:00:00Z",
"verificationMethod": "did:solidus:testnet:verify-service#key-1",
"proofPurpose": "assertionMethod",
"proofValue": "z3FXQje2Y..."
}
}On-chain: A BLAKE3 hash of the credential is written to the Credential Store smart contract. This creates an immutable anchor proving the credential was issued at a specific time, without storing any personal data on-chain.
Off-chain: The full credential (with personal data) is returned only to the user and stored in their wallet.
4. Store the Credential
The user stores the credential in their Solidus Identity wallet or any compatible storage.
// Option A: Store in the Solidus Identity app
// The credential is automatically saved when the user completes
// verification through the hosted flow.
// Option B: Store locally in your application
import fs from 'fs'
// Save the credential to a secure local store
fs.writeFileSync(
'./credentials/kyc-credential.json',
JSON.stringify(credential, null, 2)
)
// Option C: Store in an encrypted database
await db.credentials.create({
did: did.id,
type: 'KYCCredential',
credential: JSON.stringify(credential),
issuedAt: new Date(credential.issuanceDate),
expiresAt: new Date(credential.expirationDate),
})On-chain: Nothing. Credential storage is entirely off-chain.
Off-chain: The credential is stored by the holder. Only the holder decides when and where to present it.
5. Present the Credential
When a relying party (such as a DeFi protocol or an exchange) asks for proof of identity, the user creates a Verifiable Presentation. This bundles one or more credentials with a proof that the presenter controls the DID.
import { createAuthClient } from '@solidus-network/auth'
// The relying party creates a challenge
const challengeResponse = await fetch('https://relying-party.com/api/auth/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requiredCredentials: ['KYCCredential'],
}),
})
const challenge = await challengeResponse.json()
// The user signs a presentation with their private key
const authClient = createAuthClient({
privateKey: privateKeyHex,
did: did.id,
})
const presentation = await authClient.signPresentation({
challenge: challenge.challengeId,
credentials: [credential],
})
// Send the presentation to the relying party
const verifyResponse = await fetch('https://relying-party.com/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ presentation }),
})The presentation structure looks like this:
{
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiablePresentation"],
"holder": "did:solidus:testnet:7Hk3mRtQZv...",
"verifiableCredential": [
{ "...the full KYC credential..." }
],
"proof": {
"type": "Ed25519Signature2020",
"created": "2026-05-07T14:30:00Z",
"challenge": "challenge-id-from-relying-party",
"domain": "relying-party.com",
"verificationMethod": "did:solidus:testnet:7Hk3...#key-1",
"proofPurpose": "authentication",
"proofValue": "z4ABCde..."
}
}On-chain: Nothing during presentation. The relying party will verify on-chain in the next step.
Off-chain: The user signs the presentation with their private key, binding the credentials to the specific challenge and domain. This prevents replay attacks.
6. Verify the Credential
The relying party verifies the presentation and its contained credentials. Verification checks four things: the cryptographic signature, the expiration date, the revocation status, and the on-chain anchor.
import { createSdk } from '@solidus-network/sdk'
import { createAuthVerifier } from '@solidus-network/auth'
const sdk = createSdk({
mode: 'testnet',
chain: {
rpcUrl: 'https://rpc.solidus.network',
signerPrivateKey: process.env.SOLIDUS_PRIVATE_KEY!,
network: 'testnet',
},
})
const verifier = createAuthVerifier({
rpcUrl: 'https://rpc.solidus.network',
})
// Verify the presentation (proves the user controls the DID)
const presentationResult = await verifier.verifyPresentation(presentation)
console.log('Presentation valid:', presentationResult.valid)
console.log('Subject DID:', presentationResult.subject)
// Verify each credential (checks signature, expiry, revocation, anchor)
for (const cred of presentationResult.credentials) {
const result = await sdk.credentials.verify(cred)
console.log(\`Credential ${cred.id}:\`)
console.log(' Valid:', result.valid)
console.log(' Checks:', result.checks)
// => ['signature', 'expiration', 'revocation', 'onChainAnchor']
}On-chain: The verifier reads the DID Registry to get the issuer’s public key, then reads the Credential Store to confirm the credential hash exists and has not been revoked. These are read-only queries.
Off-chain: Signature verification and expiration checks happen in memory. The verifier never contacts the issuer directly.
7. Revoke a Credential
If a credential needs to be invalidated (for example, when a user’s identity information changes or fraud is detected), the issuer can revoke it on-chain.
// Only the original issuer can revoke a credential
const issuerSdk = createSdk({
mode: 'testnet',
chain: {
rpcUrl: 'https://rpc.solidus.network',
signerPrivateKey: process.env.ISSUER_PRIVATE_KEY!,
network: 'testnet',
},
})
// Revoke the credential
const revocation = await issuerSdk.credentials.revoke({
credentialId: credential.id,
reason: 'Information no longer accurate',
})
console.log('Revoked:', revocation.success)
console.log('Transaction:', revocation.txHash)
// => 0x7a3b...
// After revocation, verification will fail
const result = await sdk.credentials.verify(credential)
console.log('Valid:', result.valid) // => false
console.log('Checks:', result.checks)
// 'revocation' check will failOn-chain: A revocation entry is written to the Credential Store. The credential hash is marked as revoked, and future verification queries will see this status.
Off-chain: The credential data itself is not modified. The holder still possesses the credential, but any verifier checking on-chain will see it is revoked.
Summary
| Step | On-chain | Off-chain |
|---|---|---|
| Create DID | DID Document written to registry | Private key stored locally |
| Complete KYC | Nothing | Document upload, OCR, liveness check |
| Receive credential | Credential hash anchored | Full credential returned to holder |
| Store credential | Nothing | Holder saves to wallet/database |
| Present credential | Nothing | Holder signs a Verifiable Presentation |
| Verify credential | Read DID Registry + Credential Store | Signature and expiration checks |
| Revoke credential | Revocation entry written | Nothing |
Next Steps
- KYC Integration — embed the verification flow in your app
- Webhooks — receive notifications when verification completes
- How Solidus Works — protocol architecture and consensus