Keytrace #
Identity verification for ATProto. Link your decentralized identity (DID) to external accounts like GitHub, LinkedIn, Instagram, DNS, and Mastodon with cryptographically signed attestations.
What is Keytrace? #
Keytrace allows Bluesky users to prove ownership of external accounts by:
- Creating a claim - Post a verification token to your GitHub gist, DNS TXT record, or other supported platform
- Verification - Keytrace fetches and validates the proof contains your DID
- Attestation - A cryptographic signature is created and stored in your ATProto repo as a
dev.keytrace.claimrecord
Claims are user-owned, portable, and stored directly in your ATProto repository.
Project Structure #
keytrace/
├── apps/
│ └── keytrace.dev/ # Nuxt 3 web application
├── packages/
│ ├── runner/ # Core verification library (@keytrace/runner)
│ └── lexicon/ # ATProto lexicon schemas
Development #
# Install dependencies
yarn install
# Start development server
yarn dev
# Run tests
yarn test
# Type checking
yarn typecheck
# Format code
yarn format
Developing the API #
If you're working on the API, you'll need to install and run Tap to maintain a backfilled list of all claim records for us:
# Install tap on your machine
go install github.com/bluesky-social/indigo/cmd/tap@latest
# Run tap, only syncing keytrace claim records
TAP_SIGNAL_COLLECTION=dev.keytrace.claim TAP_COLLECTION_FILTERS='dev.keytrace.claim' tap run --disable-acks=true
# Run the server with a TAP_URL set
KEYTRACE_TAP_URL=http://127.0.0.1:2480 yarn dev
If you need to check backfill, or test ingestion, remove both the Keytrace and Tap databases:
rm tap.db apps/keytrace.dev/.data/reverse-lookup.sqlite
Production (Railway / Docker) #
In production the Nuxt server and Tap run together inside one container, supervised by a small Node process at apps/host/. The Railway start command should be:
node apps/host/index.mjs
The optionl included Dockerfile builds the Tap Go binary, builds the Nuxt app, and produces an image whose CMD already runs the supervisor. The image expects a Railway Volume mounted at /keytrace-data so tap.db and reverse-lookup.sqlite survive redeploys
Environment variables consumed by the supervisor:
KEYTRACE_DATA_DIR— directory fortap.dbandreverse-lookup.sqlite(default/keytrace-datain the image)TAP_BIN— path to the tap binary (defaulttap, on$PATHin the image)TAP_HOST/TAP_PORT— where tap should be reached (defaults127.0.0.1/2480)KEYTRACE_SERVER_ENTRY— override the Nitro server entry path (defaults to the builtapps/keytrace.dev/.output/server/index.mjs)
How Verification Works #
The runner package implements a recipe-based verification system:
- Service Providers match claim URIs to verification strategies
- Recipes define verification steps as JSON specifications
- Verification Steps are composable actions:
http-get,dns-txt,css-select,json-path,regex-match
Example flow:
User submits gist URL
→ Match URI to GitHub provider
→ Execute recipe: HTTP GET → CSS select → regex match for DID
→ Extract identity metadata (username, avatar)
→ Create attestation signature
→ Write dev.keytrace.claim to user's ATProto repo
ATProto Lexicons #
dev.keytrace.claim- Identity claim linking a DID to an external accountdev.keytrace.recipe- Verification recipe specificationdev.keytrace.key- Daily signing key for attestationsdev.keytrace.signature- Cryptographic attestation structure
Deployment #
Publishing Packages #
Use the deploy script to bump versions and publish all packages to npm:
./scripts/deploy.sh patch # 0.0.1 → 0.0.2
./scripts/deploy.sh minor # 0.0.2 → 0.1.0
./scripts/deploy.sh major # 0.1.0 → 1.0.0
This will:
- Bump versions in
@keytrace/runner,@keytrace/claims, and@keytrace/lexicon - Build all packages
- Publish to npm
- Create a git commit and tag
After running, push to remote:
git push && git push --tags
Adding a New Service Provider #
Want to add support for a new platform (e.g., GitLab, Codeberg, Tangled.) The key requirement is that the platform must have some way for users to publicly post text content that Keytrace can fetch and verify — things like profile bios, public posts, gists, comments, or files.
Proof of Identity Pattern #
Every service provider follows the same pattern:
- The user places a proof string (containing their DID) somewhere publicly readable on the service
- Keytrace fetches that public URL and checks that the proof string is present
- Metadata (username, avatar, profile URL) is extracted from the response
Good proof locations include: public gists/snippets, profile bios, DNS TXT records, public repos, pinned posts, or any content the user controls that's fetchable via HTTP.
You need to be careful that it's a place where only the identity can post. For example you can post a GitHub gist with a keytrace DID but the comments can also contain keytrace DIDs for other people. This could be used to make a false claim.
What You Need to Create #
All you need to touch is the packages/runner/ package — the web app picks up new providers automatically via the /api/services endpoint and a shared useServiceRegistry composable.
-
Create a provider file in
packages/runner/src/serviceProviders/implementing theServiceProviderinterface:id,name,homepage— basic metadatareUri— regex to match claim URIsui— wizard configuration (icon, instructions, proof template, input labels)processURI()— converts a matched URI into fetch + verification configpostprocess()— extracts identity metadata (username, avatar, profile URL)getProofText()— generates the proof string for the usertests— URI match test cases
-
Register it in
packages/runner/src/serviceProviders/index.ts -
Icon: Set
ui.iconto a Lucide icon name (e.g.,"github","globe","shield") and the web app renders it automatically. If your service needs a custom SVG icon, add a component toapps/keytrace.dev/components/icons/and register it in theiconMapinapps/keytrace.dev/composables/useServiceRegistry.ts. Setui.iconDisplay: "raw"for standalone SVGs (like npm/tangled) that shouldn't be wrapped in a circular badge.
Using Claude Code to Add a Provider #
From the repo root, try a prompt like:
Add a new service provider for [ServiceName]. Users will prove their identity by [describe the proof location, e.g. "creating a public snippet on GitLab containing their DID", or "adding their DID to their Codeberg profile bio"]. The proof URL format is [e.g. "https://gitlab.com/-/snippets/:id"]. Look at the existing providers in
packages/runner/src/serviceProviders/for the pattern — especiallygithub.tsfor an HTTP+JSON example ordns.tsfor a simpler one. Register the new provider in the index file and add URI match tests.
That should give Claude Code enough to:
- Create a new file in
packages/runner/src/serviceProviders/ - Implement the
ServiceProviderinterface (URI regex,processURI,uiconfig,getProofText, test cases) - Register it in
packages/runner/src/serviceProviders/index.ts
What Goes in the PR #
When you open your PR, please include:
- How to create a proof: Step-by-step instructions for how a user creates the public proof on the service (e.g., "go to gitlab.com/-/snippets/new, paste this, make it public")
- Example proof URL: A real or realistic example URL so I can test the flow
- Any API quirks: Rate limits, auth requirements, non-standard response formats, CORS issues, etc.
- Fetcher needs: The existing fetchers are
http,dns, andactivitypub. If your service needs something different, note that
License #
MIT
- TypeScript 67%
- Vue 32%
- CSS <1%
- Shell <1%
- Docker <1%