SoulBound Tokens (Legacy)
SBTs are being phased out in favor of attestations. For new integrations, use the API Reference or Attestation Protocols.
SoulBound Tokens (SBTs) are non-transferable tokens that were the original method for recording Individual Verifications on-chain. While SBTs are still issued for backward compatibility, attestations are now the primary verification record.
Overview
SBTs are:
- Non-transferable: Cannot be sent to another address
- Tied to identity: Represent a verified individual
- Time-limited: Expire after 1 year and must be renewed
When to Use SBTs
Consider querying SBTs only if:
- You have an existing integration that depends on SBT contracts
- You need to verify historical verifications issued before the attestation migration
For all new integrations, use Attestation Protocols instead.
Contract Addresses
Optimism (EVM)
| Contract | Address |
|---|---|
| Hub V3 | 0x2AA822e264F8cc31A2b9C22f39e5551241e94DfB |
Optimism Sepolia (Testnet)
| Contract | Address |
|---|---|
| Hub V3 | 0x71712Ac158C5b25519Aad43eB9f496ae16892211 |
Stellar (Soroban)
| Contract | Address |
|---|---|
| SBT Contract | CCNTHEVSWNDOQAMXXHFOLQIXWUINUPTJIM6AXFSKODNVXWA4N7XV3AI5 |
SBT Status Codes
All chains use consistent status codes when querying SBTs:
| Status | Description |
|---|---|
valid | SBT exists and is active |
expired | SBT has passed its expiration date (1 year from issuance) |
revoked | SBT has been revoked |
none | No SBT found for this address |
Querying SBTs on Stellar
Off-Chain Verification (TypeScript)
Use the Stellar SDK to query a user’s SBT status:
import {
rpc,
TransactionBuilder,
Networks,
Contract,
scValToNative,
nativeToScVal,
} from '@stellar/stellar-sdk'
type StellarSbt = {
action_nullifier: bigint
circuit_id: bigint
expiry: bigint
id: bigint
minter: string
public_values: Array<bigint>
recipient: string
revoked: boolean
}
type StellarSbtStatus = 'valid' | 'expired' | 'revoked' | 'none'
const sorobanRpcUrl = 'https://mainnet.sorobanrpc.com'
const sbtContractAddress = 'CCNTHEVSWNDOQAMXXHFOLQIXWUINUPTJIM6AXFSKODNVXWA4N7XV3AI5'
async function getStellarSBT(
address: string,
circuitId: string
): Promise<{ sbt?: StellarSbt; status: StellarSbtStatus }> {
const sorobanServer = new rpc.Server(sorobanRpcUrl)
const userAccount = await sorobanServer.getAccount(address)
const contract = new Contract(sbtContractAddress)
const operation = contract.call(
'get_sbt',
nativeToScVal(address, { type: 'address' }),
nativeToScVal(circuitId, { type: 'u256' })
)
const transaction = new TransactionBuilder(userAccount, {
networkPassphrase: Networks.PUBLIC,
fee: '100',
})
.addOperation(operation)
.setTimeout(60)
.build()
const response = await sorobanServer.simulateTransaction(transaction)
if (rpc.Api.isSimulationSuccess(response)) {
const parsed = rpc.parseRawSimulation(response)
const sbt = scValToNative(parsed.result?.retval)
return { sbt, status: 'valid' }
}
const error = response.error
if (error?.includes('Error(Contract, #1)')) return { status: 'none' }
if (error?.includes('Error(Contract, #5)')) return { status: 'revoked' }
if (error?.includes('Error(Contract, #6)')) return { status: 'expired' }
throw new Error(`SBT query failed: ${error}`)
}On-Chain Verification (Soroban)
1. Fetch the SBT contract WASM:
soroban contract fetch \
--id CCNTHEVSWNDOQAMXXHFOLQIXWUINUPTJIM6AXFSKODNVXWA4N7XV3AI5 \
-o ./passport_sbt_contract.wasm \
--network mainnet \
--network-passphrase "Public Global Stellar Network ; September 2015" \
--rpc-url https://mainnet.sorobanrpc.com2. Import and use in your contract:
#![no_std]
use soroban_sdk::{contract, contractimpl, Env, Address, U256};
mod passport_sbt_contract {
soroban_sdk::contractimport!(
file = "../../passport_sbt_contract.wasm"
);
}
#[contract]
pub struct MyContract;
#[contractimpl]
impl MyContract {
pub fn get_sbt(
env: Env,
recipient: Address,
circuit_id: U256
) -> passport_sbt_contract::SBT {
let contract_addr = Address::from_str(
&env,
"CCNTHEVSWNDOQAMXXHFOLQIXWUINUPTJIM6AXFSKODNVXWA4N7XV3AI5"
);
let client = passport_sbt_contract::Client::new(&env, &contract_addr);
client.get_sbt(&recipient, &circuit_id)
}
}Circuit IDs
Different verification types use different circuit IDs when querying SBTs:
| Verification Type | Circuit ID |
|---|---|
| Government ID (KYC) | 0x729d660e1c02e4e419745e617d643f897a538673ccf1051e093bbfa58b0a120b |
| Phone | 0xbce052cf723dca06a21bd3cf838bc518931730fb3db7859fc9cc86f0d5483495 |
| Biometrics | 0x0b5121226395e3b6c76eb8ddfb0bf2f2075e7f2c6956567e84b38a223c3a3d15 |
| ePassport | 0xf2ce248b529343e105f7b3c16459da619281c5f81cf716d28f7df9f87667364d |
Migration to Attestations
If you have an existing SBT integration, we recommend migrating to attestations:
- API users: No changes needed - the API will automatically use attestations
- Direct contract queries: Switch to Sign Protocol queries
For migration assistance, contact us .
Additional Resources
- Attestation Protocols - Recommended integration method
- API Reference - Simple verification status checks
- Stellar Documentation  - Stellar-specific guides