Documentation Index
Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
Use this file to discover all available pages before exploring further.
Agent Name Service (ANS) is GoDaddy’s open registry for naming and verifying AI agents. It pairs DNS-style identifiers (for example, ans://v1.0.0.my-agent.example.com) with X.509 identity certificates and an append-only transparency log, so any party can resolve an agent name and confirm who is on the other end.
ANS gives an agent a name and a verifiable identity. Dynamic gives that same agent a wallet that can sign and transact. Combined, you hand counterparties a single human-readable identifier they can use to verify who an agent is and which wallet it pays from — before any onchain action happens.
This page shows the end-to-end pattern: create a Dynamic server wallet, register the wallet under an ANS name, then resolve and verify other ANS-named agents before paying or trusting them. Code samples are provided for Node, Python, and Rust — pick the SDK that matches your stack.
The ANS request/response shapes below follow the v2 RA API and the transparency-log API from the GoDaddy ANS reference implementation — spec/api-spec-v2.yaml and spec/api-spec-tl-v2.yaml. The flow here was validated end-to-end against that implementation run locally. There is no official Node or Python ANS SDK yet, so those samples call the REST API directly; official SDKs exist for Rust, Go, and Java.
Why combine them
| Capability | Dynamic | ANS |
|---|
| Wallet creation + onchain signing (MPC) | ✓ | — |
| Human-readable, DNS-style agent name | — | ✓ |
| X.509 identity certificate | — | ✓ |
| Public transparency log of identity events | — | ✓ |
| Onchain payments (x402, MPP) | ✓ | — |
For agent-to-agent flows — especially anything that touches money — pairing the two lets the recipient resolve an ans:// name, validate the agent’s certificate against the transparency log, and only then accept its onchain signature as authoritative.
Prerequisites
- A Dynamic Environment ID and API token from the Dynamic Dashboard
- Access to an ANS Registration Authority (RA) and a bearer token for it. Obtain one from agentnameregistry.org, or run the reference RA locally (see the note below).
- A domain you control — ANS performs DNS-based ownership verification before issuing a certificate
- Runtime for the SDK you choose: Node.js 22+, Python 3.10+, or Rust 1.76+
Install the SDKs:
npm install @dynamic-labs-wallet/node-evm @dynamic-labs-wallet/node @peculiar/x509 @peculiar/webcrypto
pip install dynamic-wallet-sdk cryptography httpx
[dependencies]
dynamic-waas-sdk = "0.0.3"
dynamic-waas-sdk-evm = "0.0.3"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
rcgen = "0.13"
sha2 = "0.10"
serde_json = "1"
Set environment variables:
DYNAMIC_ENVIRONMENT_ID=your_environment_id
DYNAMIC_AUTH_TOKEN=your_api_token
# ANS Registration Authority. The base URL INCLUDES the API version
# segment (.../v2), per the ANS OpenAPI spec.
ANS_API_BASE=https://api.ans.example.org/v2
# Transparency Log base (verifier reads live under /v1).
ANS_TL_BASE=https://tl.example.org
# Sent as `Authorization: Bearer <token>`.
ANS_API_TOKEN=your_ans_bearer_token
Auth is a bearer token, sent as Authorization: Bearer <token> (the RA also accepts GoDaddy’s Authorization: sso-key <apiKey>:<apiSecret> form) — there is no X-API-Key header. To try the flow locally, clone github.com/godaddy/ans, run docker compose up --build -d followed by make docker-compose-bootstrap, then set ANS_API_BASE=http://localhost:18080/v2, ANS_TL_BASE=http://localhost:18081, and ANS_API_TOKEN=ans-dev-key-change-me.
Step 1: Create a Dynamic server wallet for the agent
This wallet is what the agent will use to sign onchain payments. Persist the returned key shares in your secrets manager.
import { DynamicEvmWalletClient } from '@dynamic-labs-wallet/node-evm';
import { ThresholdSignatureScheme } from '@dynamic-labs-wallet/node';
const client = new DynamicEvmWalletClient({
environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
});
await client.authenticateApiToken(process.env.DYNAMIC_AUTH_TOKEN!);
const { walletMetadata, externalServerKeyShares } = await client.createWalletAccount({
thresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
password: process.env.WALLET_PASSWORD!,
backUpToDynamic: true,
});
console.log('Agent wallet:', walletMetadata.accountAddress);
import asyncio
import os
from dynamic_wallet_sdk import DynamicEvmWalletClient, ThresholdSignatureScheme
async def create_agent_wallet():
async with DynamicEvmWalletClient(os.environ["DYNAMIC_ENVIRONMENT_ID"]) as client:
await client.authenticate_api_token(os.environ["DYNAMIC_AUTH_TOKEN"])
wallet = await client.create_wallet_account(
threshold_signature_scheme=ThresholdSignatureScheme.TWO_OF_TWO,
password=os.environ["WALLET_PASSWORD"],
)
print(f"Agent wallet: {wallet.account_address} (id: {wallet.wallet_id})")
return wallet
asyncio.run(create_agent_wallet())
use dynamic_waas_sdk::{
DynamicWalletClient, DynamicWalletClientOpts, ThresholdSignatureScheme,
};
use dynamic_waas_sdk_evm::DynamicEvmWalletClient;
#[tokio::main]
async fn main() -> dynamic_waas_sdk::Result<()> {
let mut client = DynamicWalletClient::new(
DynamicWalletClientOpts::new(&std::env::var("DYNAMIC_ENVIRONMENT_ID").unwrap()),
)?;
client
.authenticate_api_token(&std::env::var("DYNAMIC_AUTH_TOKEN").unwrap())
.await?;
let evm = DynamicEvmWalletClient::new(&client);
let (wallet_properties, external_server_key_shares) = evm
.create_wallet_account(
ThresholdSignatureScheme::TwoOfTwo,
Some(std::env::var("WALLET_PASSWORD").unwrap()),
/* back_up_to_dynamic */ true,
)
.await?;
println!("Agent wallet: {}", wallet_properties.account_address);
Ok(())
}
The returned key shares (externalServerKeyShares / external_server_key_shares) are MPC signing materials. Store them in your secrets manager — never log them, commit them, or send them to ANS.
Step 2: Publish an agent card that includes the wallet
The Agent Card is the off-chain metadata document an ANS endpoint points at. This is where the Dynamic wallet and the ANS identity meet: the card declares which onchain account this named agent pays from, so any counterparty resolving the ANS name can decide whether to accept it before transacting.
https://my-agent.example.com/.well-known/agent-card.json
{
"name": "ans://v1.0.0.my-agent.example.com",
"version": "1.0.0",
"description": "Pays for data feeds via x402.",
"wallet": {
"address": "0xabc...",
"chains": ["base", "tempo"],
"paymentProtocols": ["x402", "mpp"]
},
"endpoints": {
"service": "https://my-agent.example.com/api"
}
}
Serve this from the same domain you register — both the certificate’s identity and the card URL must resolve to a host you control. You’ll reference it from the registration in Step 4 as an endpoint’s metaDataUrl, alongside a metaDataHash that pins its contents:
import { createHash } from 'node:crypto';
const cardBytes = Buffer.from(JSON.stringify(agentCard));
const metaDataUrl = 'https://my-agent.example.com/.well-known/agent-card.json';
const metaDataHash = `SHA256:${createHash('sha256').update(cardBytes).digest('hex')}`;
Step 3: Generate the X.509 CSRs
ANS issues X.509 certificates from Certificate Signing Requests. Use a separate keypair from the Dynamic wallet — the Dynamic wallet key signs onchain transactions, while the X.509 identity key signs mTLS handshakes between agents. The link between the two identities is the wallet address you publish in the agent card (Step 2).
Two important requirements the RA enforces:
- Keys must be EC P-256 (ECDSA
secp256r1). RSA CSRs are rejected.
- The identity CSR’s Subject Alternative Name must be a URI equal to the agent’s
ans:// name. The optional server (TLS) CSR’s SAN is the agent host FQDN.
import 'reflect-metadata'; // required by @peculiar/x509
import * as x509 from '@peculiar/x509';
import { Crypto } from '@peculiar/webcrypto';
const crypto = new Crypto();
x509.cryptoProvider.set(crypto);
const ansName = 'ans://v1.0.0.my-agent.example.com';
const host = 'my-agent.example.com';
const keygen: EcKeyGenParams = { name: 'ECDSA', namedCurve: 'P-256' };
const sign: EcdsaParams = { name: 'ECDSA', hash: 'SHA-256' };
async function makeCsr(commonName: string, san: x509.JsonGeneralName) {
const keys = await crypto.subtle.generateKey(keygen, true, ['sign', 'verify']);
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
name: `CN=${commonName}`,
keys,
signingAlgorithm: sign,
extensions: [new x509.SubjectAlternativeNameExtension([san])],
});
return { csrPem: csr.toString('pem'), privateKey: keys.privateKey };
}
// Identity CSR: URI SAN must equal the ans:// name.
const identity = await makeCsr(ansName, { type: 'url', value: ansName });
// Server CSR: DNS SAN = the agent host.
const server = await makeCsr(host, { type: 'dns', value: host });
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID
ans_name = "ans://v1.0.0.my-agent.example.com"
host = "my-agent.example.com"
def make_csr(common_name: str, san: x509.GeneralName):
key = ec.generate_private_key(ec.SECP256R1())
csr = (
x509.CertificateSigningRequestBuilder()
.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)]))
.add_extension(x509.SubjectAlternativeName([san]), critical=False)
.sign(key, hashes.SHA256())
)
pem = csr.public_bytes(serialization.Encoding.PEM).decode()
return pem, key
# Identity CSR: URI SAN must equal the ans:// name.
identity_csr, identity_key = make_csr(ans_name, x509.UniformResourceIdentifier(ans_name))
# Server CSR: DNS SAN = the agent host.
server_csr, server_key = make_csr(host, x509.DNSName(host))
use rcgen::{CertificateParams, DnType, KeyPair, SanType, PKCS_ECDSA_P256_SHA256};
let ans_name = "ans://v1.0.0.my-agent.example.com";
let host = "my-agent.example.com";
fn make_csr(common_name: &str, san: SanType) -> rcgen::Result<(String, KeyPair)> {
let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
let mut params = CertificateParams::new(vec![])?;
params.distinguished_name.push(DnType::CommonName, common_name);
params.subject_alt_names = vec![san];
let csr_pem = params.serialize_request(&key_pair)?.pem()?;
Ok((csr_pem, key_pair))
}
// Identity CSR: URI SAN must equal the ans:// name.
let (identity_csr, identity_key) = make_csr(ans_name, SanType::URI(ans_name.try_into()?))?;
// Server CSR: DNS SAN = the agent host.
let (server_csr, server_key) = make_csr(host, SanType::DnsName(host.try_into()?))?;
The CSR private keys are the agent’s mTLS identity material. Keep them in your secrets manager — they are not sent to ANS (only the CSRs are).
Step 4: Register the agent and activate it
Registration is a multi-step, asynchronous flow:
POST {ANS_API_BASE}/ans/agents returns 202 PENDING_VALIDATION with an agentId. All later calls address the agent by that agentId (a UUID), not by name.
POST .../verify-acme proves domain control and issues the identity + server certificates → PENDING_DNS.
POST .../verify-dns confirms the published DNS records → ACTIVE.
const headers = {
'Authorization': `Bearer ${process.env.ANS_API_TOKEN}`,
'Content-Type': 'application/json',
};
const base = process.env.ANS_API_BASE!; // ends in /v2
// 1. Register (async — returns 202 PENDING_VALIDATION)
const reg = await fetch(`${base}/ans/agents`, {
method: 'POST',
headers,
body: JSON.stringify({
agentDisplayName: 'my-agent',
agentDescription: 'Pays for data feeds via x402.',
version: '1.0.0',
agentHost: host,
endpoints: [{
agentUrl: `https://${host}/mcp`,
protocol: 'MCP', // A2A | MCP | HTTP-API
transports: ['SSE'],
metaDataUrl, // the agent card from Step 2
metaDataHash, // SHA256:<hex> of the card bytes
}],
identityCsrPEM: identity.csrPem,
serverCsrPEM: server.csrPem, // or BYOC: serverCertificatePEM + chain
}),
});
if (!reg.ok) throw new Error(`ANS register: ${await reg.text()}`);
const { agentId, ansName, status } = await reg.json();
console.log('Registered:', ansName, status, agentId); // PENDING_VALIDATION
// 2. verify-acme → issues certs → PENDING_DNS
await fetch(`${base}/ans/agents/${agentId}/verify-acme`, { method: 'POST', headers });
// 3. verify-dns → ACTIVE
await fetch(`${base}/ans/agents/${agentId}/verify-dns`, { method: 'POST', headers });
const detail = await fetch(`${base}/ans/agents/${agentId}`, { headers }).then((r) => r.json());
console.log('Status:', detail.agentStatus); // ACTIVE
// The issued identity certificate(s):
const certs = await fetch(
`${base}/ans/agents/${agentId}/certificates/identity`, { headers },
).then((r) => r.json());
console.log('Identity cert subject:', certs[0]?.certificateSubject);
import os
import httpx
base = os.environ["ANS_API_BASE"] # ends in /v2
headers = {"Authorization": f"Bearer {os.environ['ANS_API_TOKEN']}"}
async with httpx.AsyncClient(headers=headers) as http:
# 1. Register (async — returns 202 PENDING_VALIDATION)
reg = await http.post(f"{base}/ans/agents", json={
"agentDisplayName": "my-agent",
"agentDescription": "Pays for data feeds via x402.",
"version": "1.0.0",
"agentHost": host,
"endpoints": [{
"agentUrl": f"https://{host}/mcp",
"protocol": "MCP", # A2A | MCP | HTTP-API
"transports": ["SSE"],
"metaDataUrl": meta_data_url, # the agent card from Step 2
"metaDataHash": meta_data_hash,
}],
"identityCsrPEM": identity_csr,
"serverCsrPEM": server_csr, # or BYOC: serverCertificatePEM + chain
})
reg.raise_for_status()
agent = reg.json()
agent_id = agent["agentId"]
print("Registered:", agent["ansName"], agent["status"]) # PENDING_VALIDATION
# 2. verify-acme → issues certs → PENDING_DNS, then 3. verify-dns → ACTIVE
await http.post(f"{base}/ans/agents/{agent_id}/verify-acme")
await http.post(f"{base}/ans/agents/{agent_id}/verify-dns")
detail = (await http.get(f"{base}/ans/agents/{agent_id}")).json()
print("Status:", detail["agentStatus"]) # ACTIVE
use reqwest::Client;
use serde_json::json;
let base = std::env::var("ANS_API_BASE")?; // ends in /v2
let token = std::env::var("ANS_API_TOKEN")?;
let http = Client::new();
// 1. Register (async — returns 202 PENDING_VALIDATION)
let reg: serde_json::Value = http
.post(format!("{base}/ans/agents"))
.bearer_auth(&token)
.json(&json!({
"agentDisplayName": "my-agent",
"agentDescription": "Pays for data feeds via x402.",
"version": "1.0.0",
"agentHost": host,
"endpoints": [{
"agentUrl": format!("https://{host}/mcp"),
"protocol": "MCP", // A2A | MCP | HTTP-API
"transports": ["SSE"],
"metaDataUrl": meta_data_url, // the agent card from Step 2
"metaDataHash": meta_data_hash,
}],
"identityCsrPEM": identity_csr,
"serverCsrPEM": server_csr, // or BYOC: serverCertificatePEM + chain
}))
.send().await?.error_for_status()?.json().await?;
let agent_id = reg["agentId"].as_str().unwrap().to_string();
println!("Registered: {} {}", reg["ansName"], reg["status"]); // PENDING_VALIDATION
// 2. verify-acme → issues certs → PENDING_DNS, then 3. verify-dns → ACTIVE
for step in ["verify-acme", "verify-dns"] {
http.post(format!("{base}/ans/agents/{agent_id}/{step}"))
.bearer_auth(&token).send().await?.error_for_status()?;
}
ANS appends the issuance event to a public transparency log and seals it into a SCITT receipt — which is what a counterparty verifies in the next step.
If the RA has no server CA configured it rejects serverCsrPEM (and registration requires exactly one of serverCsrPEM / serverCertificatePEM). Either point it at a server CA, or bring your own server certificate via serverCertificatePEM + serverCertificateChainPEM. Field names and status codes follow spec/api-spec-v2.yaml.
Step 5: Resolve and verify a counterparty agent
Before paying or trusting another agent, verify its identity against the transparency log. Resolving the agent’s ans:// name via DNS yields its transparency-log endpoint and agentId; from there you fetch the badge and the SCITT receipt, verify the receipt’s COSE signature and Merkle inclusion proof against the log’s published /root-keys, and only then trust the wallet address from its agent card.
const tl = process.env.ANS_TL_BASE!;
const counterparty = 'ans://v1.0.0.vendor.example.com';
const host = counterparty.replace(/^ans:\/\/v[0-9.]+\./, ''); // vendor.example.com
const agentId = await resolveAgentId(counterparty); // via ANS DNS discovery
// Badge reports the agent's current lifecycle status from the log.
const badge = await fetch(`${tl}/v1/agents/${agentId}`).then((r) => r.json());
if (badge.status !== 'ACTIVE') throw new Error(`agent is ${badge.status}`);
// SCITT COSE_Sign1 receipt — CBOR tag 18, so the first byte is 0xd2.
const receipt = Buffer.from(
await fetch(`${tl}/v1/agents/${agentId}/receipt`).then((r) => r.arrayBuffer()),
);
if (receipt[0] !== 0xd2) throw new Error('not a COSE_Sign1 receipt');
// Verify the receipt's signature + Merkle inclusion against the TL's
// root keys. Use an official verifier (ans-verify / ans-sdk-*) or a
// COSE library; see spec/api-spec-tl-v2.yaml.
const rootKeys = await fetch(`${tl}/root-keys`).then((r) => r.text());
await verifyReceipt(receipt, rootKeys); // your verifier
// Only now trust the wallet from the card served by the verified host.
const card = await fetch(`https://${host}/.well-known/agent-card.json`).then((r) => r.json());
console.log('Verified payee wallet:', card.wallet.address);
import re
tl = os.environ["ANS_TL_BASE"]
counterparty = "ans://v1.0.0.vendor.example.com"
host = re.sub(r"^ans://v[0-9.]+\.", "", counterparty) # vendor.example.com
agent_id = await resolve_agent_id(counterparty) # via ANS DNS discovery
async with httpx.AsyncClient() as http:
# Badge reports the agent's current lifecycle status from the log.
badge = (await http.get(f"{tl}/v1/agents/{agent_id}")).json()
assert badge["status"] == "ACTIVE", f"agent is {badge['status']}"
# SCITT COSE_Sign1 receipt — CBOR tag 18, so the first byte is 0xd2.
receipt = (await http.get(f"{tl}/v1/agents/{agent_id}/receipt")).content
assert receipt[:1] == b"\xd2", "not a COSE_Sign1 receipt"
root_keys = (await http.get(f"{tl}/root-keys")).text
await verify_receipt(receipt, root_keys) # your verifier
card = (await http.get(f"https://{host}/.well-known/agent-card.json")).json()
print("Verified payee wallet:", card["wallet"]["address"])
let tl = std::env::var("ANS_TL_BASE")?;
let agent_id = resolve_agent_id("ans://v1.0.0.vendor.example.com").await?; // ANS DNS discovery
// Badge reports the agent's current lifecycle status from the log.
let badge: serde_json::Value = http.get(format!("{tl}/v1/agents/{agent_id}"))
.send().await?.error_for_status()?.json().await?;
assert_eq!(badge["status"], "ACTIVE");
// SCITT COSE_Sign1 receipt — CBOR tag 18, so the first byte is 0xd2.
let receipt = http.get(format!("{tl}/v1/agents/{agent_id}/receipt"))
.send().await?.error_for_status()?.bytes().await?;
assert_eq!(receipt.first(), Some(&0xd2), "not a COSE_Sign1 receipt");
let root_keys = http.get(format!("{tl}/root-keys"))
.send().await?.error_for_status()?.text().await?;
verify_receipt(&receipt, &root_keys).await?; // ans-sdk-rust verifier
With the counterparty’s wallet pinned to its verified identity, hand that address to your x402 or Tempo MPP client and pay with confidence that the receiving wallet really belongs to the named agent.
End-to-end: a paid call between two named agents
A common shape for production agent-to-agent commerce:
- Buyer agent has a Dynamic server wallet and an
ans://buyer.example.com registration.
- Seller agent has its own Dynamic wallet and
ans://seller.example.com.
- Buyer resolves
seller.example.com, validates the transparency-log receipt, fetches the seller’s agent card, and confirms the published wallet matches the payTo field in the seller’s 402 response.
- Buyer signs the 402 payment via its Dynamic wallet and retries the request.
- Both sides hold a verifiable record of who paid whom, anchored in ANS.
Lifecycle: renewal and revocation
ANS certificates have a finite lifetime. Plan for renewal — submit a new CSR to the certificate-renewal endpoints (.../certificates/server/renewal) before expiry; the same wallet stays in the agent’s metadata.
If a wallet is compromised or you retire the agent, revoke the ANS record so resolvers stop trusting it. Revoke by agentId, and pass a reason from the revocation-reason enum (KEY_COMPROMISE, CESSATION_OF_OPERATION, AFFILIATION_CHANGED, SUPERSEDED, CERTIFICATE_HOLD, PRIVILEGE_WITHDRAWN, AA_COMPROMISE):
await fetch(`${base}/ans/agents/${agentId}/revoke`, {
method: 'POST',
headers,
body: JSON.stringify({ reason: 'KEY_COMPROMISE', comments: 'key rotation' }),
});
async with httpx.AsyncClient(headers=headers) as http:
await http.post(
f"{base}/ans/agents/{agent_id}/revoke",
json={"reason": "KEY_COMPROMISE", "comments": "key rotation"},
)
http.post(format!("{base}/ans/agents/{agent_id}/revoke"))
.bearer_auth(&token)
.json(&json!({ "reason": "KEY_COMPROMISE", "comments": "key rotation" }))
.send().await?.error_for_status()?;
After revocation, rotate the underlying Dynamic wallet (create a new server wallet with a fresh threshold key set) before re-registering under a new ANS version. Never reuse signing materials across a revocation boundary.
Additional Resources