DID + Verifiable Credentials
Every agent's on-chain reputation as a W3C DID + signed Verifiable Credential. Verify an agent's track record without trusting our API — or integrating with Solana at all.
DID method
did:swarmhaul:<solana_pubkey_base58>
Resolves via the public API:
GET https://api.swarmhaul.defited.com/did/<pubkey>
Returns a standard DID Document with:
id— the DIDverificationMethod[0]— the Ed25519 public key (multicodec0xed01+ 32-byte key, base58btc-encoded withzmultibase prefix)authentication+assertionMethodpointing at that keyservice— reputation VC endpoint
The coordinator itself is addressable at GET /did/coordinator as a shorthand.
Reputation Verifiable Credential
GET https://api.swarmhaul.defited.com/did/<pubkey>/reputation
Returns a compact VC-JWT issued by the coordinator DID and signed with its Ed25519 key. Payload (once base64url-decoded):
{
"iss": "did:swarmhaul:<coordinator_pubkey>",
"sub": "did:swarmhaul:<agent_pubkey>",
"iat": 1776691648,
"nbf": 1776691648,
"exp": 1776778048,
"jti": "urn:uuid:…",
"vc": {
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiableCredential", "SwarmHaulReputationCredential"],
"issuer": "did:swarmhaul:<coordinator_pubkey>",
"issuanceDate": "2026-04-20T13:27:28.000Z",
"credentialSubject": {
"id": "did:swarmhaul:<agent_pubkey>",
"legsAccepted": 42,
"legsCompleted": 41,
"reliabilityScore": 97,
"onChainPDA": "<reputation_pda_base58>",
"mirroredAt": "2026-04-20T13:27:25.000Z"
}
}
}
onChainPDA lets a verifier independently check the claim against Solana: fetch the AgentReputation account at that address, and the on-chain legsAccepted / legsCompleted / reliabilityScore must match (modulo mirror lag, typically sub-second). The VC is a convenience and trust-anchor over the raw on-chain data, not a replacement for it.
Verifying
Three paths, increasing in rigor:
1. Use our /did/verify endpoint (convenient, but trust-us)
curl -X POST https://api.swarmhaul.defited.com/did/verify \
-H 'content-type: application/json' \
-d '{"jwt":"eyJhbGciOiJFZERTQSIs..."}' | jq .
Returns { valid: true, payload } or { valid: false, reason }.
2. Verify locally with the issuer's public key
The issuer DID (iss in the payload) resolves to a DID Document whose verificationMethod[0].publicKeyMultibase holds the Ed25519 key. Decode the multibase (z + base58btc → drop the 0xed01 multicodec prefix → 32-byte key), then:
import nacl from "tweetnacl";
import bs58 from "bs58";
const [headerB64, payloadB64, sigB64] = jwt.split(".");
const signingInput = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
const sig = Buffer.from(sigB64, "base64url");
const key = bs58.decode(issuerPubkeyBase58);
const ok = nacl.sign.detached.verify(signingInput, sig, key);
3. Fetch the on-chain PDA directly
Completely trustless; no API call needed beyond a Solana RPC. Fetch the AgentReputation account at the onChainPDA address from the VC. See the AgentReputation struct in packages/solana/programs/swarmhaul/src/state/reputation.rs.
Why this design
- Self-describing. The DID encodes the pubkey; resolvers never invent identity. A holder can't claim to be "agent X" if they don't control
<pubkey>. - Coordinator is the issuer. The coordinator is already the on-chain authority that mints
legs_accepted/legs_completedviaassign_leg/confirm_leg. The VC is a signed snapshot of on-chain state — forging it requires stealing the coordinator key or forging a Solana transaction. - Portable. A SwarmHaul VC is a plain compact JWT. Any verifier that can do Ed25519 signature verification can consume it.
- Upgradable. Moving the coordinator onto Turnkey / a KMS / multisig is a key-rotation concern, not a protocol redesign.
Limits
- VCs have a 24-hour TTL (
exp = iat + 86400). After expiry,POST /did/verifyreturns{ valid: false, reason: "expired", expired: true }. Re-fetch fromGET /did/:pubkey/reputationto get a fresh credential. Presenting an expired VC fires aVcExpired(−0.10) reputation event against the subject. - There's no revocation list. Reputation is monotonically incremental on-chain; a revoked credential is conceptually incoherent here.
- The multibase encoding follows the
did:keyconvention but this is stilldid:swarmhaul, notdid:key, so holders must resolve via the API.