How to Build Digital Credentials using Solana Attestation Service

The Solana Attestation Service (SAS) is a powerful on-chain credentialing system that enables developers to create, manage, and verify digital attestations on the Solana blockchain. Think of it as a decentralized way to issue and verify credentials, certificates, badges, or any form of digital proof that someone or something meets certain criteria.
SAS introduces powerful capabilities for associating offchain data with on-chain accounts, building trust and identity systems, including:
Attestations can then be consumed by service providers (e.g., DEXs, DAOs, or other platforms) to vet users and gate services based on their credentials (e.g., enable VIP Members to access unique content on a website or Accredited Investors to participate in certain on-chain investment opportunities).
This guide will walk you through creating a complete attestation script that:
The end goal will be to see a working attestation system in action:
Starting Solana Attestation Service Demo
1. Setting up wallets and funding payer...
- Airdrop completed: 384wkVUZsyuk53Npyy5N29tWTRA6dGe82b6fpBa4gKMDHoZYsb3iNAUfYMD6Lo2V3MYJeDhk8xvEDrmyxjeW2xdB
2. Creating Credential...
- Credential created - Signature: 5LnkP762S9yvcLxFUVU7N3Mzen5Tqp8abC4h1rJYZn1vCviq7GpyFvUNVneVd8btiV7KK6pe5NEpXvwtTXK96gM1
- Credential PDA: 3yB9Xrgg73oWxuQv8564q9LwwRL2rX2fjZD7ssy3X4M3
3. Creating Schema...
- Schema created - Signature: 4qHfY6FfjsUrssymRDWgShgr2sxRgWTbSdHxnJhgus7Cra5t6n6f4snhPDDkMyAX9bkqpD7aMCbKUpoJnD9NXzoS
- Schema PDA: FfNqeLfPHy4p7FPgH2LDTm9gzVWSDcupA3LUhiMEzBXw
4. Creating Attestation...
- Attestation created - Signature: 3czDWMmDkZEJbww7qfphcKe96vJJMAawFk2DedbknrzVpBSJ5fGBjtEK1aZHsYzvj8QLvRcadohEaxANNb4c4nUN
- Attestation PDA: 6JEEL89jNXvxk63N6ND8njsp6e1Ve8BLZYShYNfjFajR
5. Updating Authorized Signers...
- Authorized signers updated - Signature: 23V3bmTYnKUA6fT9WnchFmKi7bNj9biqxxnLBW9coUtkWhippu49PrY5fnefAcHWKofNDoojCicD2qJFq16RNz1Y
6. Verifying Attestations...
- Attestation data: { name: 'test-user', age: 100, country: 'usa' }
- Test User is verified
- Random User is not verified
7. Closing Attestation...
- Closed attestation - Signature: 5DS7GYpzKirWcusEgBhN3LGfX7D34q5rSQmbdNpCmzU5nYM1CUA4fJ3B9DRXwuiYNHvMnbBRSiMGDVJoCfLMc6ti
Solana Attestation Service demo completed successfully!
Before starting this tutorial, ensure you have:
SAS provides a standardized way to create verifiable claims about entities (users, organizations, or even other programs) directly on Solana.
The attestation system consists of three main components:
Each of these components is represented on-chain as an account governed by the SAS Program: 22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG.
Let's start by creating our project structure:
mkdir solana-attestation-starter && cd solana-attestation-starter
Initialize a new Node.js project with the required dependencies
pnpm init
pnpm i sas-lib gill
pnpm i -D typescript ts-node @types/node
Create a tsconfig.json file:
{
"compilerOptions": {
"target": "es2020",
"module": "nodenext",
"lib": ["es2020"],
"declaration": true,
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "nodenext"
},
"rootDir": "./",
"outDir": "./dist",
"include": ["./"],
"exclude": ["node_modules", "dist"]
}
Update your package.json scripts:
{
"scripts": {
"start": "ts-node attestation.ts",
"build": "tsc"
}
}
Let's create our attestation.ts file and build it step by step:
Start with the necessary imports and configuration:
import {
getCreateCredentialInstruction,
getCreateSchemaInstruction,
serializeAttestationData,
getCreateAttestationInstruction,
fetchSchema,
getChangeAuthorizedSignersInstruction,
fetchAttestation,
deserializeAttestationData,
deriveAttestationPda,
deriveCredentialPda,
deriveSchemaPda,
deriveEventAuthorityAddress,
getCloseAttestationInstruction,
SOLANA_ATTESTATION_SERVICE_PROGRAM_ADDRESS,
} from 'sas-lib'
import {
airdropFactory,
generateKeyPairSigner,
lamports,
Signature,
TransactionSigner,
Instruction,
Address,
Blockhash,
createSolanaClient,
createTransaction,
SolanaClient,
} from 'gill'
import {
estimateComputeUnitLimitFactory,
} from 'gill/programs'
const CONFIG = {
CLUSTER_OR_RPC: 'devnet',
CREDENTIAL_NAME: 'TEST-ORGANIZATION',
SCHEMA_NAME: 'THE-BASICS',
SCHEMA_LAYOUT: Buffer.from([12, 0, 12]),
SCHEMA_FIELDS: ['name', 'age', 'country'],
SCHEMA_VERSION: 1,
SCHEMA_DESCRIPTION: 'Basic user information schema for testing',
ATTESTATION_DATA: {
name: 'Evan',
age: 25,
country: 'USA',
},
ATTESTATION_EXPIRY_DAYS: 365,
}
This sets up our basic configuration, including:
devnet for this demo.Before moving on, let's examine the Schema Layout and Fields we defined above. The SCHEMA_LAYOUT array defines the data types for each field in our attestation, where each number corresponds to a specific data type from the Solana Attestation Service's type system. In our example, Buffer.from([12, 0, 12]) maps to our three fields:
12 represents a String type for the "name" field,0 represents a U8 integer type for the "age" field, and12 represents another String type for the "country" field.The SCHEMA_FIELDS array provides human-readable names that correspond one-to-one with the layout types. This approach ensures that when attestations are created, the data is properly typed and validated according to the schema definition. The available data types range from basic integers (U8, U16, U32, U64, U128) and signed integers (I8-I128) to booleans, characters, strings, and even vectors of these types, giving you flexibility in designing your attestation data structures. Check out the full list of options and mapping here.
Next, let's add a couple of utility functions to handle keypair management and transaction confirmations:
async function setupWallets(client: SolanaClient) {
try {
const payer = await generateKeyPairSigner() // or loadKeypairSignerFromFile(path.join(process.env.PAYER))
const authorizedSigner1 = await generateKeyPairSigner()
const authorizedSigner2 = await generateKeyPairSigner()
const issuer = await generateKeyPairSigner()
const testUser = await generateKeyPairSigner()
const airdrop = airdropFactory({ rpc: client.rpc, rpcSubscriptions: client.rpcSubscriptions })
const airdropTx: Signature = await airdrop({
commitment: 'processed',
lamports: lamports(BigInt(1_000_000_000)),
recipientAddress: payer.address
})
console.log(` - Airdrop completed: ${airdropTx}`)
return { payer, authorizedSigner1, authorizedSigner2, issuer, testUser }
} catch (error) {
throw new Error(`Failed to setup wallets: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
Our setupWallets function creates five keypairs and airdrops SOL to our payer account:
These utility functions handle:
This should create five keypair files in the wallets directory:
payer will be used to pay our transaction feesissuer will be the authority that creates our credentialauthorizedSigner keys will be authorized to create attestationstestUser will receive the attestation we createNext add a reusable function that will help us easily create and send transactions. Add the following to your code:
async function sendAndConfirmInstructions(
client: SolanaClient,
payer: TransactionSigner,
instructions: Instruction[],
description: string,
): Promise<Signature> {
try {
const simulationTx = createTransaction({
version: 'legacy',
feePayer: payer,
instructions: instructions,
latestBlockhash: {
blockhash: '11111111111111111111111111111111' as Blockhash,
lastValidBlockHeight: 0n,
},
computeUnitLimit: 1_400_000,
computeUnitPrice: 1,
})
const estimateCompute = estimateComputeUnitLimitFactory({ rpc: client.rpc })
const computeUnitLimit = await estimateCompute(simulationTx)
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send()
const tx = createTransaction({
version: 'legacy',
feePayer: payer,
instructions: instructions,
latestBlockhash,
computeUnitLimit,
computeUnitPrice: 1, // In production, use dynamic pricing
})
const signature = await client.sendAndConfirmTransaction(tx)
console.log(` - ${description} - Signature: ${signature}`)
return signature
} catch (error) {
throw new Error(`Failed to ${description.toLowerCase()}: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
Our function does two things:
Now let's add a verification function that demonstrates how to check if a user has a valid attestation. To verify a user's attestation, we will need to pass two pieces of information: the user's address (or other identifier associated with the user) and the Schema address that you want to validate against. Add the verifyAttestation function to your code and then we will walk through how it works:
async function verifyAttestation({
client,
schemaPda,
userAddress
}: {
client: SolanaClient
schemaPda: Address
userAddress: Address
}): Promise<boolean> {
try {
const schema = await fetchSchema(client.rpc, schemaPda)
if (schema.data.isPaused) {
console.log(` - Schema is paused`)
return false
}
const [attestationPda] = await deriveAttestationPda({
credential: schema.data.credential,
schema: schemaPda,
nonce: userAddress,
})
const attestation = await fetchAttestation(client.rpc, attestationPda)
const attestationData = deserializeAttestationData(schema.data, attestation.data.data as Uint8Array)
console.log(` - Attestation data:`, attestationData)
const currentTimestamp = BigInt(Math.floor(Date.now() / 1000))
return currentTimestamp < attestation.data.expiry
} catch (error) {
return false
}
}
This function:
Additionally, if you know it, you could pass a credentialPda as a double check that your schema is correctly associated with the known credential.
Now, let's build the main demonstration function that showcases the complete attestation workflow. Add the main function to your code -- we'll use this to outline for our steps:
async function main() {
console.log('Starting Solana Attestation Service Demo\n')
const client: SolanaClient = createSolanaClient({ urlOrMoniker: CONFIG.CLUSTER_OR_RPC })
// Step 1: Setup wallets and fund payer
console.log('1. Setting up wallets and funding payer...')
const { payer, authorizedSigner1, authorizedSigner2, issuer, testUser } = await setupWallets(client)
// Step 2: Create Credential
// Step 3: Create Schema
// Step 4: Create Attestation
// Step 5: Update Authorized Signers
// Step 6: Verify Attestations
// Step 7. Close Attestation
}
main()
.then(() => console.log('\nSolana Attestation Service demo completed successfully!'))
.catch((error) => {
console.error('❌ Demo failed:', error)
process.exit(1)
})
The current code includes spaces for 7 steps. We've started by creating our client and calling our setupWallets function. Let's add the remaining steps next.
First, we will need to create our Credential. This establishes the issuing authority (like a certification body) with initial authorized signers.
Add the following code to your main function:
// Step 2: Create Credential
console.log('\n2. Creating Credential...')
const [credentialPda] = await deriveCredentialPda({
authority: issuer.address,
name: CONFIG.CREDENTIAL_NAME,
})
const createCredentialInstruction = getCreateCredentialInstruction({
payer,
credential: credentialPda,
authority: issuer,
name: CONFIG.CREDENTIAL_NAME,
signers: [authorizedSigner1.address]
})
await sendAndConfirmInstructions(client, payer, [createCredentialInstruction], 'Credential created')
console.log(` - Credential PDA: ${credentialPda}`)
Here, we just need to use a couple of helper function sas-lib package:
deriveCredentialPda: derives the PDA of our credential account based on the authority (our issuer wallet) and the name of our credential (meaning an issuer could create multiple credentials by creating new ones with different names) (ref).getCreateCredentialInstruction: will assemble the instruction to create the credential account (ref). For now, we will just add a single authorized signer, so that we can add our second signer later.We send our instruction to the network by calling our sendAndConfirmInstructions helper function.
Next, we need to define our Schema to define the structure of data that can be attested (name, age, country in our example). Add the following to your code:
// Step 3: Create Schema
console.log('\n3. Creating Schema...')
const [schemaPda] = await deriveSchemaPda({
credential: credentialPda,
name: CONFIG.SCHEMA_NAME,
version: CONFIG.SCHEMA_VERSION,
})
const createSchemaInstruction = getCreateSchemaInstruction({
authority: issuer,
payer,
name: CONFIG.SCHEMA_NAME,
credential: credentialPda,
description: CONFIG.SCHEMA_DESCRIPTION,
fieldNames: CONFIG.SCHEMA_FIELDS,
schema: schemaPda,
layout: CONFIG.SCHEMA_LAYOUT,
})
await sendAndConfirmInstructions(client, payer, [createSchemaInstruction], 'Schema created')
console.log(` - Schema PDA: ${schemaPda}`)
We follow a similar pattern here--derive PDA, assemble instruction, and send it to the network using sendAndConfirmInstructions:
deriveSchemaPda: derives the PDA of our schema account based on the credential we created in Step 2, the name of our schema, and the version (ref).getCreateSchemaInstruction: will assemble the instruction to create the schema account (ref).Next, we need to issue an actual attestation to a specific user with the defined data and expiration. Add the following to Step 4 of your main function:
// Step 4: Create Attestation
console.log('\n4. Creating Attestation...')
const [attestationPda] = await deriveAttestationPda({
credential: credentialPda,
schema: schemaPda,
nonce: testUser.address,
})
const schema = await fetchSchema(client.rpc, schemaPda)
const expiryTimestamp = Math.floor(Date.now() / 1000) + (CONFIG.ATTESTATION_EXPIRY_DAYS * 24 * 60 * 60)
const createAttestationInstruction = await getCreateAttestationInstruction({
payer,
authority: authorizedSigner1,
credential: credentialPda,
schema: schemaPda,
attestation: attestationPda,
nonce: testUser.address,
expiry: expiryTimestamp,
data: serializeAttestationData(schema.data, CONFIG.ATTESTATION_DATA),
})
await sendAndConfirmInstructions(client, payer, [createAttestationInstruction], 'Attestation created')
console.log(` - Attestation PDA: ${attestationPda}`)
Like before, we will first derive our Attestation PDA using a helper function, deriveAttestationPda (ref). Note that the parameters include a nonce which can be a user's address or some other unique identifier address (e.g. some value stored off-chain).
Prior to calling getCreateAttestationInstruction, we need to do a couple of things:
fetchSchema to get the deserialized schema data which is required to serialize our attestation data. We do so by passing the schema data and our attestation into serializeAttestationData.Notice that the authority in our instruction parameters is authorizedSigner1, the authorized signer we defined in Step 2 when we created our credential.
Finally, we send the instruction to the network to create the attestation PDA.
We mentioned earlier that we can update a credential's authorized signers. Let's add authorizedSigner2 as an authorized signer to our credential to demonstrate how to manage who can issue attestations for the credential. Add the following to Step 5 of your main function:
// Step 5: Update Authorized Signers
console.log('\n5. Updating Authorized Signers...')
const changeAuthSignersInstruction = await getChangeAuthorizedSignersInstruction({
payer,
authority: issuer,
credential: credentialPda,
signers: [authorizedSigner1.address, authorizedSigner2.address],
})
await sendAndConfirmInstructions(client, payer, [changeAuthSignersInstruction], 'Authorized signers updated')
We just need to assemble an instruction passing our new signers array (this replaces the old list of signers, so make sure to include existing signers you wish to keep) into getChangeAuthorizedSignersInstruction and send it to the network with our helper function.
Let's run a verification check to show how you might check if users have valid attestations, testing both attested and non-attested users. Normally, you might run something like this on your backend when a user signs into your platform. Add the following to Step 6 of your main function:
// Step 6: Verify Attestations
console.log('\n6. Verifying Attestations...')
const isUserVerified = await verifyAttestation({
client,
schemaPda,
userAddress: testUser.address,
})
console.log(` - Test User is ${isUserVerified ? 'verified' : 'not verified'}`)
const randomUser = await generateKeyPairSigner()
const isRandomVerified = await verifyAttestation({
client,
schemaPda,
userAddress: randomUser.address,
})
console.log(` - Random User is ${isRandomVerified ? 'verified' : 'not verified'}`)
Here, we are calling our helper function verifyAttestation twice: once with our testUser address (which we would expect to be verified at this point) and a new randomUser who does not have an attestation (we would expect this user's verification to fail).
Finally, let's revoke an attestation from a user. Add the following code to your project as Step 7:
// Step 7. Close Attestation
console.log('\n7. Closing Attestation...')
const eventAuthority = await deriveEventAuthorityAddress()
const closeAttestationInstruction = await getCloseAttestationInstruction({
payer,
attestation: attestationPda,
authority: authorizedSigner1,
credential: credentialPda,
eventAuthority,
attestationProgram: SOLANA_ATTESTATION_SERVICE_PROGRAM_ADDRESS,
})
await sendAndConfirmInstructions(client, payer, [closeAttestationInstruction], 'Closed attestation')
In order to generate an attesation instruction, we need to get the Event Authority address using deriveEventAuthorityAddress. The event authority is used to emit close events in the SAS program. We then assemble our instruction using the getCloseAttestationInstruction -- note that we must pass one of our authorizedSigner addresses into the authority parameter.
To test your attestation workflow, run the script in your project terminal:
pnpm start
Here's what you should expect to see in the output:
Starting Solana Attestation Service Demo
1. Setting up wallets and funding payer...
- Airdrop completed: 4QE4VGMxnvU9psgjDCYSoRsGEcdZzSsnqFKdzgTf5tPt2i833TC2gLdZE6QZfphie4S9MXNgJEVpQhvwgQJG5Bd5
2. Creating Credential...
- Credential created - Signature: 2hb857yRrxficGU6zCMEvMwf6uKTASfgswgmwx1VS9z6zUL66FoapXMNK5VV7P3cZ6HktBMETp2Pu85EvSvUu1dr
- Credential PDA: 14Bfygrnpj7bA5H8gvAU3Jr1dfpudZFr2QkJSUFUqoYp
3. Creating Schema...
- Schema created - Signature: 3Jo9pQgPcAJj1AmYm6weR9t4tw4Unq2qTxjS5przBUbwt94TLGm1sDaV7z5pBzt1Kyw4oxuCYg6NkUBijVs6vJ4X
- Schema PDA: EyzsLnwtcCXrPJ8bSWHopRRNj3nGicXwuj8McfskToNs
4. Creating Attestation...
- Attestation created - Signature: 2UJAcwTEF98xge1HPfZicYiBW4NqQDrtBiLcvAWWJ9sx9x4XyYZD4XGuwfkero11Mw5X9fhFzb7QTAMGvewDyZek
- Attestation PDA: B2CQ5uqsHgV9QYcCdqfeZGEWNJijTTFgNutNVcFjae8D
5. Updating Authorized Signers...
- Authorized signers updated - Signature: 3h7x8y8v5fGyV368b6DxDuJ6HpD73whAGVYNJLmTQPk3HezgZcWqsMXPcHqiYM9NQzwHJgCQmhyqnteE1yHVrDDu
6. Verifying Attestations...
- Attestation data: { name: 'Evan', age: 25, country: 'USA' }
- Test User is verified
- Random User is not verified
7. Closing Attestation...
- Closed attestation - Signature: 5DS7GYpzKirWcusEgBhN3LGfX7D34q5rSQmbdNpCmzU5nYM1CUA4fJ3B9DRXwuiYNHvMnbBRSiMGDVJoCfLMc6ti
Solana Attestation Service demo completed successfully!
Congratulations! You've successfully implemented a complete Solana Attestation Service system. You now have a working demonstration that shows how to:
The Solana Attestation Service provides a powerful foundation for building trust and identity systems on Solana. Whether you're creating compliance systems, financial credentials, professional certifications, or gaming achievements, SAS gives you the tools to issue and verify claims in a decentralized, transparent way.