One handle, one lookup. Everything below runs against the live API at
https://username.md — no signup needed for reads.
The full surface is specified in OpenAPI 3.1. Runnable versions of every sample on this page live in the examples repo.
One URL, four representations. The Accept header picks the
format: HTML for browsers, JSON for integrations, Markdown for terminals and LLMs,
JSON-LD (schema.org/Person) for knowledge graphs, or a signed JWS bundle you can verify
offline. In a browser, ?accept=json overrides the header.
curl -s -H 'accept: application/json' https://username.md/v1/users/chris/profile
curl -s -H 'accept: text/markdown' https://username.md/v1/users/chris/profile
curl -s -H 'accept: application/ld+json' https://username.md/v1/users/chris/profile
curl -s -H 'accept: application/jose+json' https://username.md/v1/users/shannon/profileimport requests
url = "https://username.md/v1/users/chris/profile"
profile = requests.get(url, headers={"Accept": "application/json"}).json()
print(profile["handle"], profile["did"])
markdown = requests.get(url, headers={"Accept": "text/markdown"}).text
person = requests.get(url, headers={"Accept": "application/ld+json"}).json() # schema.org/Personconst url = "https://username.md/v1/users/chris/profile";
const profile = await (await fetch(url, { headers: { accept: "application/json" } })).json();
console.log(profile.handle, profile.did);
const markdown = await (await fetch(url, { headers: { accept: "text/markdown" } })).text();
const person = await (await fetch(url, { headers: { accept: "application/ld+json" } })).json();req, _ := http.NewRequest("GET", "https://username.md/v1/users/chris/profile", nil)
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
var profile struct {
Handle string `json:"handle"`
Did string `json:"did"`
}
json.NewDecoder(resp.Body).Decode(&profile)
fmt.Println(profile.Handle, profile.Did)Every API response is signed with the platform's Ed25519 key
(RFC 9421 HTTP Message Signatures +
RFC 9530 Content-Digest). Check three
response headers and you can prove a payload came from username.md — even if it reached you
through caches, proxies, or another agent. The verification key is published at
/.well-known/http-message-signatures-directory.
# See the signature headers on any response
curl -sI -H 'accept: application/json' https://username.md/v1/users/chris/profile \
| grep -iE 'content-digest|signature'
# Content-Digest: sha-256=:<base64(sha256(body))>:
# Signature-Input: sig1=("content-digest" "@authority");created=...;keyid="platform-ed25519";alg="ed25519"
# Signature: sig1=:<base64(ed25519 signature)>:import base64, hashlib, requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
host = "username.md"
jwks = requests.get(f"https://{host}/.well-known/http-message-signatures-directory").json()
jwk = next(k for k in jwks["keys"] if k["kid"] == "platform-ed25519")
pub = Ed25519PublicKey.from_public_bytes(base64.urlsafe_b64decode(jwk["x"] + "=="))
resp = requests.get(f"https://{host}/v1/users/chris/profile", headers={"Accept": "application/json"})
digest = resp.headers["Content-Digest"]
assert digest == "sha-256=:" + base64.b64encode(hashlib.sha256(resp.content).digest()).decode() + ":"
params = resp.headers["Signature-Input"].removeprefix("sig1=")
sig = base64.b64decode(resp.headers["Signature"][len("sig1=:"):-1])
base = f'"content-digest": {digest}\n"@authority": {host}\n"@signature-params": {params}'.encode()
pub.verify(sig, base) # raises if forgedimport { createPublicKey, createHash, verify } from "node:crypto";
const host = "username.md";
const jwks = await (await fetch(`https://${host}/.well-known/http-message-signatures-directory`)).json();
const key = createPublicKey({ key: jwks.keys.find(k => k.kid === "platform-ed25519"), format: "jwk" });
const resp = await fetch(`https://${host}/v1/users/chris/profile`, { headers: { accept: "application/json" } });
const body = Buffer.from(await resp.arrayBuffer());
const digest = resp.headers.get("content-digest");
if (digest !== `sha-256=:${createHash("sha256").update(body).digest("base64")}:`) throw new Error("digest mismatch");
const params = resp.headers.get("signature-input").replace(/^sig1=/, "");
const sig = Buffer.from(resp.headers.get("signature").slice("sig1=:".length, -1), "base64");
const base = Buffer.from(`"content-digest": ${digest}\n"@authority": ${host}\n"@signature-params": ${params}`);
if (!verify(null, base, key, sig)) throw new Error("signature FAILED");// Fetch the JWKS, then any response; rebuild the RFC 9421 §2.5 signature base.
sum := sha256.Sum256(body)
expected := "sha-256=:" + base64.StdEncoding.EncodeToString(sum[:]) + ":"
if digest != expected { log.Fatal("digest mismatch") }
params := strings.TrimPrefix(resp.Header.Get("Signature-Input"), "sig1=")
sig, _ := base64.StdEncoding.DecodeString(
strings.TrimSuffix(strings.TrimPrefix(resp.Header.Get("Signature"), "sig1=:"), ":"))
sigBase := fmt.Sprintf("%q: %s\n%q: %s\n%q: %s",
"content-digest", digest, "@authority", "username.md", "@signature-params", params)
if !ed25519.Verify(pub, []byte(sigBase), sig) { log.Fatal("signature FAILED") }A claim is a statement about a handle — "owns this domain", "controls this GitHub account", "this Bluesky identity is mine" — signed as a W3C Verifiable Credential with the handle's own Ed25519 key. The bundle includes the public key, so you can verify every claim without trusting the API.
curl -s https://username.md/v1/users/chris/claims
# → { "did": "...", "public_jwk": {...}, "claims": [ { "type": "atprotoHandle", "jws": "...", ... } ] }import base64, requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
b64url = lambda s: base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
bundle = requests.get("https://username.md/v1/users/chris/claims").json()
pub = Ed25519PublicKey.from_public_bytes(b64url(bundle["public_jwk"]["x"]))
for claim in bundle["claims"]:
signing_input, sig = claim["jws"].rsplit(".", 1)
pub.verify(b64url(sig), signing_input.encode()) # raises if forged
print("VALID:", claim["type"])import { createPublicKey, verify } from "node:crypto";
const bundle = await (await fetch("https://username.md/v1/users/chris/claims")).json();
const key = createPublicKey({ key: bundle.public_jwk, format: "jwk" });
for (const claim of bundle.claims) {
const i = claim.jws.lastIndexOf(".");
const ok = verify(null, Buffer.from(claim.jws.slice(0, i)),
key, Buffer.from(claim.jws.slice(i + 1), "base64url"));
console.log(ok ? "VALID:" : "INVALID:", claim.type);
}var bundle struct {
PublicJwk struct{ X string `json:"x"` } `json:"public_jwk"`
Claims []struct{ Type, Jws string } `json:"claims"`
}
json.NewDecoder(resp.Body).Decode(&bundle)
raw, _ := base64.RawURLEncoding.DecodeString(bundle.PublicJwk.X)
pub := ed25519.PublicKey(raw)
for _, c := range bundle.Claims {
i := strings.LastIndex(c.Jws, ".")
sig, _ := base64.RawURLEncoding.DecodeString(c.Jws[i+1:])
fmt.Println(ed25519.Verify(pub, []byte(c.Jws[:i]), sig), c.Type)
}Every handle publishes the documents agents use to find and verify it:
a did:web document (a public key record served as plain JSON), an
ATProto/Bluesky DID, and per-handle
llms.txt instructions for AI agents.
# did:web document (public keys + service endpoints)
curl -s https://username.md/users/chris/.well-known/did.json
# ATProto DID — what Bluesky queries to resolve @chris.username.md
curl -s https://username.md/users/chris/.well-known/atproto-did
# Bluesky binding status + the signed atprotoHandle credential
curl -s https://username.md/v1/users/chris/atproto
# Instructions for AI agents interacting with this handle
curl -s https://username.md/users/chris/llms.txtimport requests
BASE = "https://username.md"
did_doc = requests.get(f"{BASE}/users/chris/.well-known/did.json").json()
atproto = requests.get(f"{BASE}/v1/users/chris/atproto").json()
agent_rules = requests.get(f"{BASE}/users/chris/llms.txt").text
print(did_doc["id"]) # did:web:...
print(atproto["atproto_handle"], atproto["bound"])const BASE = "https://username.md";
const didDoc = await (await fetch(`${BASE}/users/chris/.well-known/did.json`)).json();
const atproto = await (await fetch(`${BASE}/v1/users/chris/atproto`)).json();
const agentRules = await (await fetch(`${BASE}/users/chris/llms.txt`)).text();
console.log(didDoc.id); // did:web:...
console.log(atproto.atproto_handle, atproto.bound);base := "https://username.md"
didDoc, _ := http.Get(base + "/users/chris/.well-known/did.json")
atproto, _ := http.Get(base + "/v1/users/chris/atproto")
agentRules, _ := http.Get(base + "/users/chris/llms.txt")
// decode with encoding/json as in the profile example_playing SRV / _presence TXT) are the gamer-tier surface described on
the gamers page. Until they're announced in llms.txt,
lookups return empty answers — write your integration to treat "no records" as
"not published" (never as an error) and fall back to the profile API, which is live today.
These samples run as-is: they degrade gracefully to the fallback."Is /u/chris on right now, and where do I join?" Presence
publishes as DNS SRV/TXT under the handle — the same service-discovery machinery game servers
already use, pointed at a player. Anything that can do a DNS lookup — a Discord bot, an LFG
site, a squadmate's agent — can answer it with no API keys and no polling five launchers.
Short TTLs apply, but DNS caches are still DNS caches; the click-through join link routes
through the live edge instead.
# In game right now? SRV answers with the join target
dig +short SRV _playing._tcp.chris.username.md
# → 10 0 25565 mc.chris.username.md. (Minecraft clients join this natively)
# Structured presence: live=, game=, join=, party=, next=
dig +short TXT _presence.chris.username.md
# Fallback that always answers today: who is /u/chris, and what do they play?
curl -s -H 'accept: application/json' https://username.md/v1/users/chris/profileimport dns.resolver, requests # pip install dnspython requests
def lookup(qname, rtype):
try:
return list(dns.resolver.resolve(qname, rtype))
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
return [] # not published ≠ error
zone = "chris.username.md"
for srv in lookup(f"_playing._tcp.{zone}", "SRV"):
print(f"IN GAME — join {str(srv.target).rstrip('.')}:{srv.port}")
presence = {}
for r in lookup(f"_presence.{zone}", "TXT"):
for s in r.strings:
key, _, value = s.decode().partition("=")
if value: presence[key] = value
if not presence.get("live") == "true": # fallback — live today
profile = requests.get(f"https://username.md/v1/users/chris/profile",
headers={"Accept": "application/json"}).json()
print(profile["handle"], "—", profile.get("tagline"))import { resolveSrv, resolveTxt } from "node:dns/promises"; // Node 20+, no deps
const lookup = (fn, name) => fn(name).catch(e =>
["ENOTFOUND", "ENODATA"].includes(e.code) ? [] : Promise.reject(e)); // not published ≠ error
const zone = "chris.username.md";
for (const srv of await lookup(resolveSrv, `_playing._tcp.${zone}`))
console.log(`IN GAME — join ${srv.name}:${srv.port}`);
const txt = (await lookup(resolveTxt, `_presence.${zone}`)).flat();
const presence = Object.fromEntries(txt.filter(s => s.includes("=")).map(s => s.split(/=(.*)/s)));
if (presence.live !== "true") { // fallback — live today
const profile = await (await fetch("https://username.md/v1/users/chris/profile",
{ headers: { accept: "application/json" } })).json();
console.log(profile.handle, "—", profile.tagline);
}// Go's LookupSRV builds the _playing._tcp. prefix for you.
_, srvs, err := net.DefaultResolver.LookupSRV(ctx, "playing", "tcp", "chris.username.md")
if err != nil && !strings.Contains(err.Error(), "no such host") { log.Fatal(err) }
for _, s := range srvs {
fmt.Printf("IN GAME — join %s:%d\n", strings.TrimSuffix(s.Target, "."), s.Port)
}
txts, _ := net.DefaultResolver.LookupTXT(ctx, "_presence.chris.username.md")
// parse key=value pairs; empty slice means "not published", not an error
if len(srvs) == 0 && len(txts) == 0 {
// fallback — live today: GET /v1/users/chris/profile with Accept: application/json
// (decode as in the profile example above)
}Bring your Keybase identity over in two calls: preview what would
change, then apply. Your verified social proofs become profile links, your PGP key is
served at /keybase/pgp.asc, and a third call re-verifies the proofs under
this platform's own keys — each one becomes a signed keybaseProof
Verifiable Credential your agents can check offline. Imports are additive: nothing you
already set is overwritten unless you say so.
# 1. Preview — what would an import change? (writes nothing)
curl -s -X POST https://username.md/v1/users/you/import/keybase/preview \
-H "authorization: Bearer $USERNAME_MD_TOKEN" -H 'content-type: application/json' \
-d '{"keybase_username":"yourkeybasename"}'
# 2. Apply — profile fields, verified proofs as links, PGP key
curl -s -X POST https://username.md/v1/users/you/import/keybase \
-H "authorization: Bearer $USERNAME_MD_TOKEN" -H 'content-type: application/json' \
-d '{"keybase_username":"yourkeybasename"}'
# 3. Re-verify the proofs under username.md's own keys → signed VCs
curl -s -X POST https://username.md/v1/users/you/keybase/verify \
-H "authorization: Bearer $USERNAME_MD_TOKEN"
# Public, no auth: import status + your PGP key
curl -s https://username.md/v1/users/you/keybase
curl -s https://username.md/v1/users/you/keybase/pgp.ascimport os, requests
BASE = "https://username.md"
auth = {"Authorization": f"Bearer {os.environ['USERNAME_MD_TOKEN']}"}
plan = requests.post(f"{BASE}/v1/users/you/import/keybase/preview",
headers=auth, json={"keybase_username": "yourkeybasename"}).json()
print(plan["plan"]["fields"]) # set / keep-existing / unchanged per field
result = requests.post(f"{BASE}/v1/users/you/import/keybase",
headers=auth, json={"keybase_username": "yourkeybasename"}).json()
print(result["applied"]["links_added"])
checks = requests.post(f"{BASE}/v1/users/you/keybase/verify", headers=auth).json()
print(checks["summary"]) # {"verified": n, "failed": n, "unsupported": n}const BASE = "https://username.md";
const auth = { authorization: `Bearer ${process.env.USERNAME_MD_TOKEN}`, "content-type": "application/json" };
const plan = await (await fetch(`${BASE}/v1/users/you/import/keybase/preview`, {
method: "POST", headers: auth, body: JSON.stringify({ keybase_username: "yourkeybasename" }) })).json();
const result = await (await fetch(`${BASE}/v1/users/you/import/keybase`, {
method: "POST", headers: auth, body: JSON.stringify({ keybase_username: "yourkeybasename" }) })).json();
console.log(result.applied.links_added);
const checks = await (await fetch(`${BASE}/v1/users/you/keybase/verify`, {
method: "POST", headers: auth })).json();
console.log(checks.summary);body, _ := json.Marshal(map[string]string{"keybase_username": "yourkeybasename"})
req, _ := http.NewRequest("POST", "https://username.md/v1/users/you/import/keybase", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("USERNAME_MD_TOKEN"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
var result struct {
Applied struct {
LinksAdded []map[string]string `json:"links_added"`
} `json:"applied"`
}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result.Applied.LinksAdded)Authorization: Bearer <token>
and keep it out of your shell history.# Update profile fields (partial — omitted fields stay unchanged)
curl -s -X PATCH https://username.md/v1/users/you/profile \
-H "authorization: Bearer $USERNAME_MD_TOKEN" -H 'content-type: application/json' \
-d '{"name":"Your Name","tagline":"builder","links":[{"label":"GitHub","href":"https://github.com/you"}]}'
# Set your profile theme
curl -s -X PUT https://username.md/v1/users/you/preferences \
-H "authorization: Bearer $USERNAME_MD_TOKEN" -H 'content-type: application/json' \
-d '{"theme":"terminal","accent":"#7ee787"}'
# Prove you own a domain (publish the returned TXT record, then /check)
curl -s -X POST https://username.md/v1/users/you/verifications/dns-txt \
-H "authorization: Bearer $USERNAME_MD_TOKEN" -H 'content-type: application/json' \
-d '{"domain":"yourdomain.com"}'
# Bind your Bluesky identity → @you.username.md
curl -s -X PUT https://username.md/v1/users/you/atproto \
-H "authorization: Bearer $USERNAME_MD_TOKEN" -H 'content-type: application/json' \
-d '{"did":"did:plc:your24charplcidentifier"}'import os, requests
BASE = "https://username.md"
auth = {"Authorization": f"Bearer {os.environ['USERNAME_MD_TOKEN']}"}
requests.patch(f"{BASE}/v1/users/you/profile", headers=auth,
json={"tagline": "builder", "links": [{"label": "GitHub", "href": "https://github.com/you"}]})
requests.put(f"{BASE}/v1/users/you/preferences", headers=auth,
json={"theme": "terminal", "accent": "#7ee787"})
challenge = requests.post(f"{BASE}/v1/users/you/verifications/dns-txt",
headers=auth, json={"domain": "yourdomain.com"}).json()
print("publish TXT:", challenge["record_name"], "=", challenge["record_value"])const BASE = "https://username.md";
const auth = { authorization: `Bearer ${process.env.USERNAME_MD_TOKEN}`, "content-type": "application/json" };
await fetch(`${BASE}/v1/users/you/profile`, { method: "PATCH", headers: auth,
body: JSON.stringify({ tagline: "builder", links: [{ label: "GitHub", href: "https://github.com/you" }] }) });
await fetch(`${BASE}/v1/users/you/preferences`, { method: "PUT", headers: auth,
body: JSON.stringify({ theme: "terminal", accent: "#7ee787" }) });
const challenge = await (await fetch(`${BASE}/v1/users/you/verifications/dns-txt`, {
method: "POST", headers: auth, body: JSON.stringify({ domain: "yourdomain.com" }) })).json();
console.log("publish TXT:", challenge.record_name, "=", challenge.record_value);body := strings.NewReader(`{"tagline":"builder"}`)
req, _ := http.NewRequest("PATCH", "https://username.md/v1/users/you/profile", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("USERNAME_MD_TOKEN"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()