# 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`](/docs/did-web), [DANE](/docs/dane), the [transparency log](/docs/transparency), and the keyless [`whisper verify --trustless`](/docs/verify) 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](https://www.w3.org/TR/vc-data-model-2.0/) the same way you'd issue any other agent capability claim (see [`did:web` & verifiable credentials](/docs/did-web)): 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`](/docs/dane) 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](/docs/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](/docs/transparency): 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 chain re-derived with `delv`) - see [Identity](/docs/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`](/docs/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:

```bash
# 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:

```bash
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](https://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](https://whisper.online/integrations) page and [github.com/whisper-sec](https://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](/docs/identity) - the full seven-proof chain a wallet binding rides on · [Per-agent CA](/docs/per-agent-ca) - why the key signing the wallet credential can't be borrowed from another agent's identity.
