Skip to Content
GuidesCredential Flow

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 <── Present

1. 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/abc123

On-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 fail

On-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

StepOn-chainOff-chain
Create DIDDID Document written to registryPrivate key stored locally
Complete KYCNothingDocument upload, OCR, liveness check
Receive credentialCredential hash anchoredFull credential returned to holder
Store credentialNothingHolder saves to wallet/database
Present credentialNothingHolder signs a Verifiable Presentation
Verify credentialRead DID Registry + Credential StoreSignature and expiration checks
Revoke credentialRevocation entry writtenNothing

Next Steps

Last updated on