WhisperDocs
Recipes

Pin a wallet to an identity

Bind a crypto wallet to a verifiable agent address - attributable, auditable autonomous transactions.

An autonomous agent that can sign a transaction is also an autonomous agent that can drain a wallet with nobody's name on the damage. Give a thousand agents hot wallets today and what you have is a thousand bare keypairs: an address that moved funds, and no way for anyone downstream - a counterparty, an auditor, a chain-analysis firm, you at 3 a.m. reading logs - to connect it back to which agent, whose fleet, under what policy.

On-chain forensics dead-ends at "some EOA sent this" - exactly the anonymity a hostile actor wants, and exactly the accountability gap a legitimate operator can't afford.

Whisper already gives every agent a real, verifiable identity: a routable /128, a DNSSEC-signed name, a DANE-pinned TLS key, a public transparency-log entry. This page extends that identity to cover a wallet - publish a signed statement, in the agent's own name, that says "this address is mine," so a counterparty verifies the agent and the wallet from the same public DNS lookup, and every on-chain action inherits an address that was never anonymous in the first place.

Status check. Everything below composes from primitives Whisper ships today - did:web, DANE, the transparency log, and the keyless whisper verify --trustless identity check - plus one DIY step (signing the credential with a stock JOSE library) that needs no Whisper-specific feature. Two caveats worth stating plainly: there is no single whisper pin-wallet command yet, and whisper verify does not yet take a --credential file, so the credential's own signature is checked with the stock JOSE step shown below. It does take --signature, which folds a compact ES256 JWS into the same chain walk against the agent's DNSSEC-published key; what is missing is the W3C VC envelope, not signature verification. What follows is the pattern you assemble from shipped parts, written so it drops straight into the trustless verifier the moment the CLI wraps that last step.

The mechanism: a Verifiable Credential, not a mapping table

Don't build a side database that maps agent IDs to wallet addresses - that database is itself a new thing to trust, and it isn't checkable by anyone who doesn't have an account with you. Instead, issue a Verifiable Credential the same way you'd issue any other agent capability claim (see did:web & verifiable credentials): a compact JWS whose issuer and subject are the agent's own did:web, signed with the assertionMethod key its DID document already publishes - not a new key you generate. That key is fragment-identified by its own SHA-256 hash (#<hash>, not a fixed #key-1), and it is, deliberately, the same key DANE TLSA pins for the agent's TLS leaf.

header:  {"alg":"ES256","kid":"did:web:acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online#<assertionMethod-key-hash>","typ":"JWT"}
payload: {"iss":"did:web:acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online",
          "sub":"did:web:acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online",
          "vc":{"@context":["https://www.w3.org/ns/credentials/v2"],
                "type":["VerifiableCredential","AgentWallet"],
                "credentialSubject":{
                  "chain":"eip155:1",
                  "address":"0x9F2b...c41A",
                  "purpose":"treasury"
                }},
          "nbf":1751328000}
signature: ES256 over header.payload, by the assertionMethod key

Two design choices make this hold up under scrutiny, not just look plausible:

  1. Self-issued, but not self-authenticating in isolation. The credential alone only proves "whoever holds the assertionMethod key says this wallet is theirs" - the same trust model DKIM uses for a mail domain. What makes it worth anything is that the key is not a free-floating keypair: it's the same key DANE pins under _443._tcp.<fqdn>, the same key the per-agent CA leaf was minted for, and the whole bundle sits behind a real allocated /128 in 2a04:2a01::/32 (AS219419) with reverse DNS that resolves back to the exact same name. Forging the wallet claim means forging the identity underneath it - DNSSEC, DANE, and reverse DNS all have to lie in agreement, which is the same bar every other Whisper identity proof sets.
  2. Timestamped independently of the issuer's word. Every issuance Whisper's identity plane produces - and, by extension, anything signed with that identity's key - can be checked against the transparency log: an append-only, Ed25519-signed Merkle log in the style of Certificate Transparency (RFC 6962), periodically anchored to Bitcoin via OpenTimestamps. A counterparty doesn't have to take your word for when the binding was made; they can prove the binding existed before a specific transaction did.

Publish the resulting JWS as a plain resource next to the agent's existing did.json, the same well-known-path convention did:web already uses:

https://acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online/.well-known/did.json
https://acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online/.well-known/wallets/eip155-1.jwt

Nothing about that second path is Whisper-specific plumbing - it's a static file served over the same TLS session, DNSSEC chain, and DANE pin as everything else at that name.

Verifying a pinned wallet - before you trust a transfer

A counterparty who receives (or is about to send to) 0x9F2b...c41A and wants to know "is this actually a Whisper agent's treasury wallet, or just an address someone typed into a memo" runs four checks, none of which need an account with anyone:

  1. Resolve the claimed agent name and confirm the identity is real (PTR ⇄ AAAA bijection, DNSSEC AD=YES) - see Identity.
  2. Fetch did.json, pull the publicKeyJwk of the verificationMethod entry named in assertionMethod - match it by id, never assume array position; a document can list its keys in either order (see did:web).
  3. Fetch the wallet credential, verify the ES256 signature against that key.
  4. Confirm the same key is the one DANE pins at _443._tcp.<fqdn> - if the wallet credential were signed by a different key than the one the TLS/DANE chain trusts, that's the tell of a forged binding, not a real one.

Optionally, a fifth check for anything that matters: pull the credential's transparency-log inclusion proof and confirm it predates the on-chain transaction under scrutiny - a binding minted after a suspicious transfer is a backdated cover story, not an audit trail.

Dual example: publish and verify

With stock tools

No Whisper software anywhere in this - DNS, TLS, HTTPS, and a JOSE library. One note before you copy-paste: the agent's TLS leaf is DANE-anchored, not (yet) WebPKI-anchored, so a plain curl without -k fails closed with a generic certificate error rather than the DANE-aware verification this page is demonstrating - pass -k (or a DANE-aware client) and let step 2's TLSA compare be the actual trust check, exactly as the rest of this recipe already does:

# 1 - the identity underneath the wallet is real
dig -x 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4 +short
# acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online.

# 2 - the key that will sign the wallet credential is the same key DANE pins
dig +short TLSA _443._tcp.acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online
# 3 1 1 b653a4ef...fcb82d1d

# 3 - fetch the did:web document and the wallet credential (-k: DANE-anchored, not WebPKI yet)
curl -sk https://acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online/.well-known/did.json | jq .
curl -sk https://acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online/.well-known/wallets/eip155-1.jwt

# 4 - verify the signature with any JOSE library, no Whisper API involved
python3 - <<'PY'
import json, urllib.request, ssl
from jose import jws

fqdn = "acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online"
ctx = ssl._create_unverified_context()   # DANE-anchored, not WebPKI - see step 2's TLSA compare
doc = json.load(urllib.request.urlopen(f"https://{fqdn}/.well-known/did.json", context=ctx))
vm = next(m for m in doc["verificationMethod"] if m["id"] == doc["assertionMethod"][0])   # match by id, not index
jwk = vm["publicKeyJwk"]
credential = urllib.request.urlopen(f"https://{fqdn}/.well-known/wallets/eip155-1.jwt", context=ctx).read()

payload = json.loads(jws.verify(credential, jwk, algorithms=["ES256"]))
print(payload["vc"]["credentialSubject"])   # {"chain": "eip155:1", "address": "0x9F2b...c41A", ...}
PY

# 5 - cross-check the claimed wallet's on-chain history against the binding's timestamp
curl -s "https://api.etherscan.io/api?module=account&action=txlist&address=0x9F2b...c41A" | jq '.result[0].timeStamp'

With Whisper

The identity underneath the wallet - the part any forgery has to defeat first - is verified by one shipped, keyless command. whisper verify --trustless re-derives PTR/AAAA/TLSA/did:web from the IANA DNSSEC root in-process, trusting no Whisper API, and hands you the exact key the wallet credential must be signed by:

whisper verify --trustless acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online
CHECK         RESULT  TRUST        DETAIL
dnssec        pass    DNSSEC-root  AAAA, PTR and TLSA(3 1 1) all DNSSEC-validated to the IANA root; …
dane          pass    DNSSEC-root  served leaf SPKI-SHA256 == a published TLSA pin (1 published); …
transparency  pass    DNSSEC-root  root signature verified; …
identity_doc  pass    DNSSEC-root  JWS verified against the DNSSEC-anchored key; address/fqdn/tlsa claims match the DNSSEC-validated facts
whisper: acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online is CRYPTOGRAPHICALLY PROVEN - trust anchor: DNSSEC root (IANA anchor) + DANE-EE + DNSSEC-anchored transparency/ledger keys -- Whisper API NOT trusted

That establishes the assertionMethod key. Checking the wallet credential's ES256 signature against it is the same one-line JOSE step from the stock example above - whisper verify does not yet fold an arbitrary --credential file into its chain walk. It does fold a compact ES256 JWS via --signature, verified against the key the agent publishes in DNSSEC-signed DNS, so the gap is the VC envelope specifically. If you'd like whisper to verify a pinned-wallet credential in a single command, file a request against the CLI at github.com/whisper-sec.

For a tenant checking its own fleet's bindings server-side, the control-plane verb that already returns an agent's identity bundle is the natural place this would surface:

CALL whisper.agents({op:'identity', args:{agent:'my-agent'}})

Minting the credential - the signing half - is today a DIY step with the same stock JOSE tooling shown above, run wherever your agent process already holds the key material whisper create provisioned for it. See the integrations page and github.com/whisper-sec for recipe code as this pattern gets packaged; file a request against the CLI if you want whisper to wrap the signing step directly.

Why this beats a wallet with no name on it

A wallet address with no identity behind it is unaccountable by construction - it can move funds, but nobody outside the operator can attach a fact to it stronger than "an address did this." A wallet pinned to a Whisper identity inherits everything that identity already proves: a real allocated /128 under AS219419, a DNSSEC-signed name, a DANE-pinned key, and a transparency-logged issuance history. A counterparty doesn't have to trust your bookkeeping - they resolve the name, verify the signature, and check the timestamp, the same three stock-tool steps whether they've ever heard of Whisper or not. That's the difference between "trust our dashboard" and "verify it yourself" - and for anything moving value autonomously, it's the only difference that will hold up when something goes wrong.

Next

Identity: the address is the credential - the full seven-proof chain a wallet binding rides on · Per-agent CA - why the key signing the wallet credential can't be borrowed from another agent's identity.