Pluggable on-chain identity discovery for wallet addresses. Given a wallet address and chain, discover all DIDs, SBTs, attestations, and credentials attached to it.
The consumer decides which identity types to accept and in what priority — no hardcoded assumptions, no hardcoded endpoints.
flowchart TB
A["resolveIdentities({ address, chain, providers })"] --> B{For each provider}
B --> C["SNS Provider"]
B --> D["Attestto Creds Provider"]
B --> E["Civic Provider"]
B --> F["pkh() fallback"]
C -->|"did:sns:alice.sol"| G["ResolvedIdentity[]"]
D -->|"KYC VC + SBT"| G
E -->|"Civic Pass token"| G
F -->|"did:pkh:solana:..."| G
G --> H["Unified results — ordered by provider priority"]
style A fill:#1a1a2e,stroke:#7c3aed,color:#e0e0e0
style G fill:#1a1a2e,stroke:#10b981,color:#e0e0e0
style H fill:#1a1a2e,stroke:#10b981,color:#e0e0e0
packages/
core/ → identity-resolver (engine + pkh fallback)
sns/ → @attestto/wir-sns (SNS .sol domains → did:sns)
ens/ → @attestto/wir-ens (ENS .eth domains → did:ens)
attestto-creds/ → @attestto/wir-attestto-creds (KYC, SBTs, VCs)
civic/ → @attestto/wir-civic (Civic Pass gateway tokens)
sas/ → @attestto/wir-sas (Solana Attestation Service)
WalletConnect, Dynamic, and Wagmi are crypto wallet connectors. They connect MetaMask, Phantom, and Ledger to dApps for transaction signing. Once connected, they give you an address and a signer. That's where they stop.
This package is the identity layer that comes after. It's DNS for compliance — given a wallet address, it resolves the full identity graph attached to it: DIDs, domains, KYC credentials, institutional attestations, soulbound tokens.
A traditional bank operating under SWIFT/ISO 20022 cannot interact with a DeFi protocol using WalletConnect alone. Before allowing the transaction, the protocol needs to resolve the bank's did:web or vLEI credential to verify institutional identity and compliance status. No existing wallet connector does this.
| Step | Layer | Role | Output |
| 1 | WalletConnect / Phantom | 🔌 | Address + signer |
| 2 | identity-resolver | 🔍 | DIDs, KYC status, vLEI, SBTs, domains |
| 3 | identity-bridge | 🛡️ | VP request + cryptographic verification |
| Existing connectors handle step 1. Steps 2–3 are the identity middleware that crypto wallets are missing. | |||
| WalletConnect / Dynamic / Wagmi | identity-resolver | |
|---|---|---|
| Purpose | Connect wallet, sign transactions | Resolve identities from an address |
| Input | User action (QR scan, click) | Wallet address (string) |
| Output | Signer + address | DID Documents, linked identities (alsoKnownAs), KYC status, institutional credentials |
| When | Before any interaction | After wallet connection |
| Compliance | None — no identity awareness | FATF Travel Rule, eIDAS 2.0, GLEIF vLEI ready |
- WalletConnect → connect Solana/Ethereum wallet → get address
- identity-resolver → resolve that address → find SNS domain, Attestto credentials, Civic pass, vLEI attestation
- identity-bridge → discover credential wallet extensions → request Verifiable Presentation → verify cryptographically
Step 1 uses existing connectors. Steps 2–3 are what we built — the identity middleware that MetaMask, Phantom, and every crypto wallet are currently missing. By following W3C CHAPI and DIDComm v2 standards, this stack is already compatible with the regulatory direction of eIDAS 2.0 (EU), FATF Travel Rule, and jurisdictional digital identity wallet mandates.
Several projects resolve partial identity data from addresses. None offer a pluggable, multi-chain resolution engine.
| Project | What it does | What it doesn't do |
|---|---|---|
| @talismn/on-chain-id | Resolves ENS (Ethereum) and Polkadot on-chain identity for addresses | No Solana. No pluggable provider architecture. No DID resolution, KYC, vLEI, or SBT support. |
| @onchain-id/identity-sdk | ERC734/735 identity smart contracts (Ethereum) | Ethereum-only. Contract-level SDK, not a resolver. Last published 2+ years ago. |
| SpruceKit (DIDKit) | Issue and verify VCs. Resolve DIDs via did:pkh, did:web, did:key |
Resolves a single DID — does not discover all identities attached to an address. No provider plugin system. |
| Credo-ts | Full DIDComm + OID4VP agent framework with DID resolution | Agent framework, not an address-to-identity resolver. Requires running a full agent. |
| @digitalbazaar/vc | W3C VC issuance and verification (JSON-LD) | VC operations only. No address-to-identity discovery. |
Where identity-resolver fits: Given a wallet address, no existing package answers "what DIDs, KYC credentials, vLEI attestations, SBTs, and domains are attached to this address?" across multiple chains. identity-resolver is the only pluggable engine where you pick your providers, set their priority, and get a unified ResolvedIdentity[] back — with per-provider timeouts, cancellation, and zero hardcoded endpoints.
# Core (required)
npm install identity-resolver
# Pick the providers you need
npm install @attestto/wir-sns @attestto/wir-attestto-creds @attestto/wir-civicimport { resolveIdentities } from 'identity-resolver'
import { sns } from '@attestto/wir-sns'
import { attesttoCreds } from '@attestto/wir-attestto-creds'
import { civic } from '@attestto/wir-civic'
import { pkh } from 'identity-resolver'
const identities = await resolveIdentities({
chain: 'solana',
address: 'ATTEstto1234567890abcdef...',
providers: [
attesttoCreds({
programId: 'YOUR_PROGRAM_ID',
rpcUrl: 'https://api.yourapp.com/solana-rpc',
}),
sns({
apiUrl: 'https://api.yourapp.com/sns',
resolverUrl: 'https://api.yourapp.com/resolver',
}),
civic({ apiUrl: 'https://api.yourapp.com/civic' }),
pkh(), // Fallback — always resolves, no network calls
],
})import { resolveIdentities, pkh } from 'identity-resolver'
import { ens } from '@attestto/wir-ens'
const identities = await resolveIdentities({
chain: 'ethereum',
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
providers: [
ens({ resolverUrl: 'https://api.yourapp.com/resolver' }),
pkh(),
],
})The DID Landscape Explorer uses this package in its self-assessment wizard. When a user connects a Web3 wallet, the explorer resolves all identities attached to their address and lets them pick which DID to sign with.
No hardcoded endpoints. Every provider requires explicit URLs from the consumer. No provider ever makes a network call to an endpoint you didn't configure. See SECURITY.md for the full security model.
Recommended architecture:
flowchart LR
A[Browser<br>no keys, no direct RPC] --> B[Your backend proxy<br>holds API keys, validates origin]
B --> C[Solana RPC]
B --> D[Bonfida]
B --> E[UniResolver]
B --> F[Civic]
interface ResolveOptions {
chain: Chain // 'solana', 'ethereum', or custom
address: string // Wallet address / public key
providers: IdentityProvider[] // Ordered list — defines priority
rpcUrl?: string // Global RPC override (passed to providers)
timeoutMs?: number // Per-provider timeout (default 5000ms)
stopOnFirst?: boolean // Stop after first provider returns results
signal?: AbortSignal // Cancellation
}interface ResolvedIdentity {
provider: string // Which provider found this
did: string | null // Resolved DID, if applicable
label: string // Human-readable label
type: IdentityType // 'domain' | 'sbt' | 'attestation' | 'credential' | 'did' | 'score'
meta: Record<string, unknown> // Provider-specific metadata
}| Package | Chain | What it resolves | Required options |
|---|---|---|---|
identity-resolver |
any | Core engine + pkh() fallback |
— |
@attestto/wir-sns |
Solana | SNS .sol domains → did:sns |
apiUrl, resolverUrl |
@attestto/wir-ens |
Ethereum | ENS .eth domains → did:ens |
resolverUrl |
@attestto/wir-attestto-creds |
Solana | Attestto KYC, identity SBTs, VCs | programId, rpcUrl |
@attestto/wir-civic |
Solana | Civic Pass gateway tokens | apiUrl |
@attestto/wir-sas |
Solana | Solana Attestation Service | programId, rpcUrl |
Any identity source can become a provider. Follow these steps:
Create an interface for the configuration your provider needs. All network endpoints must be required (no hardcoded URLs).
interface MyProviderOptions {
/** Your API endpoint — consumer must provide this (required) */
apiUrl: string
/** Optional filter */
category?: string
}Export a function that takes your options and returns an IdentityProvider. This is the pattern every built-in provider follows.
import type { IdentityProvider } from 'identity-resolver'
export function myProvider(options: MyProviderOptions): IdentityProvider {
return {
name: 'my-provider', // Unique name — shows up in ResolvedIdentity.provider
chains: ['solana'], // Which chains you support (use ['*'] for all chains)
resolve: async (ctx) => {
// ... (Step 3)
},
}
}The engine calls resolve(ctx) with the wallet address and chain. Your job:
- Call your data source (API, RPC, on-chain program)
- Map results to
ResolvedIdentity[] - Return
[]if nothing found — never throw
import type { IdentityProvider, ResolveContext, ResolvedIdentity } from 'identity-resolver'
export function myProvider(options: MyProviderOptions): IdentityProvider {
return {
name: 'my-provider',
chains: ['solana'],
async resolve(ctx: ResolveContext): Promise<ResolvedIdentity[]> {
// ctx.chain — 'solana', 'ethereum', etc.
// ctx.address — the wallet public key / address
// ctx.rpcUrl — optional RPC override from the consumer
// ctx.signal — AbortSignal for cancellation (pass to fetch!)
try {
const res = await fetch(
`${options.apiUrl}/lookup/${ctx.address}`,
{ signal: ctx.signal },
)
if (!res.ok) return []
const data = await res.json() as { items: Array<{ name: string; did?: string }> }
if (!data.items?.length) return []
return data.items.map((item) => ({
provider: 'my-provider', // Must match the name above
did: item.did ?? null, // DID string, or null if not applicable
label: item.name, // Human-readable label for display
type: 'credential' as const, // 'domain' | 'sbt' | 'attestation' | 'credential' | 'did' | 'score'
meta: { raw: item }, // Anything extra — consumers access via meta
}))
} catch {
return [] // Never throw — return empty on failure
}
},
}
}Pass your provider to resolveIdentities alongside any others. Order = priority.
import { resolveIdentities, pkh } from 'identity-resolver'
import { myProvider } from './my-provider'
const identities = await resolveIdentities({
chain: 'solana',
address: pubkey,
providers: [
myProvider({ apiUrl: 'https://my-backend.com/api/lookup' }),
pkh(), // always-available fallback
],
})To share your provider as an npm package:
- Name it
@yourorg/wir-<name>(convention, not required) - Add
identity-resolveras a peer dependency for type compatibility - Export your factory function + options interface
{
"name": "@yourorg/wir-my-provider",
"peerDependencies": {
"identity-resolver": "^0.1.0"
}
}| Rule | Why |
|---|---|
| No hardcoded URLs | Consumer controls infrastructure, keys, CORS |
Never throw from resolve() |
One broken provider must not crash the chain |
Return [] on failure |
Empty = nothing found, engine moves on |
Pass ctx.signal to fetch |
Supports cancellation and timeouts |
Use meta for extras |
Don't extend ResolvedIdentity — put provider-specific data in meta |
| One provider = one source | Keep providers focused (SNS, Civic, etc.) |
pnpm install
pnpm run build # Build all packages
pnpm run lint # Type-check all packagesSee CONTRIBUTING.md.
MIT