Blockchain-driven digital identity: DIDs, verifiable credentials, trust models, and building identity solutions on the XRP Ledger.
What you'll learn
So far in this track an account has been an anonymous string of base58: it can hold XRP, send payments, issue tokens. This module gives an account a name it can prove and claims someone else vouches for. By the end you'll have set a decentralized identifier on a testnet account, issued a verifiable credential from one account to another, accepted it, and read both back from the ledger — the raw materials of every "know your counterparty" feature in Web3 finance.
Two ideas do all the work here, and keeping them distinct is half the battle. A DID (decentralized identifier) is who I am — an identifier I control, with no central registry, that points at a document describing how to verify me. A credential is what someone says about me — a signed statement from an issuer ("this account passed KYC", "this account is an accredited investor") that a third party can check without phoning the issuer. You own your DID; you collect credentials about it. The XRPL gives you native transaction types for both.
We'll lean on xrpl.js and the public testnet, exactly as in the payments module. Everything below is real, runnable code.
Think of a passport. The passport number is an identifier — it points to you and nobody else. The pages inside describe how to check it: your photo, the signature, the watermarks. A DID separates those two roles cleanly. The identifier is a string; the DID document it resolves to lists the public keys and service endpoints someone needs in order to verify that you control it.
The W3C standardizes the format as did:<method>:<id>. On the XRP Ledger the method is xrpl, and the id is simply the account address — so an account rXXXX... has the identifier did:xrpl:rXXXX.... There is no separate registration step and no naming authority: the account is the identity, and control of the account's keys is control of the DID. That is what "decentralized" buys you — no company can revoke your identifier, because no company issued it.
On-ledger, a DID lives in a DID object owned by the account, and you create or update it with one transaction.
The DIDSet transaction attaches identity data to your account. It has three optional fields, and you must supply at least one:
Data — public attestations of identity credentials associated with the DID.URI — a Universal Resource Identifier pointing at off-ledger identity material (commonly where the DID document is hosted).DIDDocument — the DID document itself, stored on-ledger.All three are blobs, which on the wire means hex-encoded strings. xrpl.js ships a helper, convertStringToHex, so you never hand-encode.
First, the now-familiar setup:
import * as xrpl from 'xrpl'
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
async function main() {
await client.connect()
const { wallet: alice } = await client.fundWallet()
// ... DID and credential work goes here ...
await client.disconnect()
}
main()
Now give Alice a DID by pointing it at a hosted DID document:
const didSet: xrpl.DIDSet = {
TransactionType: 'DIDSet',
Account: alice.classicAddress,
URI: xrpl.convertStringToHex('https://example.com/alice/did.json'),
Data: xrpl.convertStringToHex('did:xrpl profile v1'),
}
const result = await client.submitAndWait(didSet, {
autofill: true,
wallet: alice,
})
const code = result.result.meta?.TransactionResult
if (code !== 'tesSUCCESS') throw new Error(`DIDSet failed: ${code}`)
console.log('DID set:', result.result.hash)
A few things the ledger enforces. If you submit a DIDSet with none of the three fields it fails with temEMPTY_DID — there must be something to store. To remove one field later, set it to an empty string; if that would leave the DID object with nothing in it, the transaction fails with tecEMPTY_DID instead, and you should use the dedicated DIDDelete transaction to tear the whole object down:
const didDelete: xrpl.DIDDelete = {
TransactionType: 'DIDDelete',
Account: alice.classicAddress,
}
Read the DID back at any time by asking the ledger for the account's objects:
const objects = await client.request({
command: 'account_objects',
account: alice.classicAddress,
type: 'did',
})
console.log(objects.result.account_objects)
That's a complete, self-sovereign identifier. But notice what it does not contain: any claim that an outside party has vouched for. Alice can say anything she likes in her own DID document. For statements others should trust, you need credentials.
A credential is a signed statement an issuer makes about a subject. The pattern has three roles, and they map cleanly onto on-ledger accounts:
The power of the model is that the verifier trusts the issuer, not the subject, and can check the credential straight from the ledger without contacting the issuer at all. It's the difference between "trust me, I'm accredited" and "this regulator's account signed a statement saying so, and here it is on-chain."
On XRPL this is the Credentials feature. Two transactions cover the happy path, and — importantly — issuance is a two-party handshake, not a one-sided stamp.
The issuer submits CredentialCreate. Its fields:
Account — the sender, which is the issuer.Subject — the account the credential is about.CredentialType — arbitrary hex-encoded data naming the kind of credential, 1–64 bytes. This is how a verifier knows it's looking at, say, a KYC pass rather than an age check.Expiration (optional) — a Unix-time timestamp after which the credential is no longer valid.URI (optional) — hex-encoded pointer to off-ledger detail about the credential, up to 256 bytes.Let's have a second account play issuer and attest that Alice passed KYC:
const { wallet: issuer } = await client.fundWallet()
const credentialType = xrpl.convertStringToHex('KYC')
const create: xrpl.CredentialCreate = {
TransactionType: 'CredentialCreate',
Account: issuer.classicAddress,
Subject: alice.classicAddress,
CredentialType: credentialType,
}
const createResult = await client.submitAndWait(create, {
autofill: true,
wallet: issuer,
})
if (createResult.result.meta?.TransactionResult !== 'tesSUCCESS') {
throw new Error('CredentialCreate failed')
}
At this point the credential exists on the ledger but is provisional. The subject hasn't agreed to it.
Why a second step? Because no one should be able to brand your account with claims you never agreed to carry. A credential only becomes valid once the subject accepts it, with CredentialAccept:
Account — the sender, which must be the subject.Issuer — the account that created the credential.CredentialType — the same hex type from the create.const accept: xrpl.CredentialAccept = {
TransactionType: 'CredentialAccept',
Account: alice.classicAddress,
Issuer: issuer.classicAddress,
CredentialType: credentialType,
}
const acceptResult = await client.submitAndWait(accept, {
autofill: true,
wallet: alice,
})
if (acceptResult.result.meta?.TransactionResult !== 'tesSUCCESS') {
throw new Error('CredentialAccept failed')
}
console.log('Credential accepted:', acceptResult.result.hash)
Only the subject can do this, and only once — accepting an already-accepted, expired, or non-existent credential fails. The result is a mutual record: the issuer chose to vouch, and the subject chose to hold the claim. When either party wants it gone, CredentialDelete removes it — and notably either the issuer or the subject can delete, so a subject is never stuck carrying a credential they no longer want.
Read the accepted credential back the same way you read the DID:
const creds = await client.request({
command: 'account_objects',
account: alice.classicAddress,
type: 'credential',
})
console.log(creds.result.account_objects)
A credential nobody checks is just data. The reason to put identity on-ledger is to gate actions on it. XRPL wires credentials into two access-control features you've met elsewhere in the track:
This is the whole point of the trust model coming full circle: the issuer attests once, the subject accepts once, and from then on the protocol itself — not application code, not a centralized gatekeeper — lets the right counterparties through. That's "compliance for Web3 finance" expressed as ledger objects rather than paperwork.
Some claims are uncertain to verify precisely from the course outline alone — for example the exact on-ledger reserve cost of a DID or credential object, or the precise schema of a stored DID document. When you build, confirm those specifics against the live testnet and the XRPL protocol references before relying on a hard number.
You're done when you have a single script that, in one run: sets a DID on a testnet account with DIDSet, issues a credential from an issuer account to a subject with CredentialCreate, has the subject accept it with CredentialAccept, and then reads both the DID object and the credential object back from the ledger via account_objects. Capture the validated hash for each of the three transactions and confirm them on the Testnet Explorer. Submit a repo or gist containing your identity.ts and the three tx hashes for a peer to verify.
Assignments
0 of 1 complete