The monorepo for a keybase on atproto
See the codeIdentity verification for ATProto. Link your decentralized identity (DID) to external accounts like GitHub, LinkedIn, Instagram, DNS, and Mastodon with cryptographically signed attestations.
Keytrace allows Bluesky users to prove ownership of external accounts by:
dev.keytrace.claim recordClaims are user-owned, portable, and stored directly in your ATProto repository.
keytrace/
├── apps/
│ └── keytrace.dev/ # Nuxt 3 web application
├── packages/
│ ├── runner/ # Core verification library (@keytrace/runner)
│ └── lexicon/ # ATProto lexicon schemas
# Install dependencies
yarn install
# Start development server
yarn dev
# Run tests
yarn test
# Type checking
yarn typecheck
# Format code
yarn format
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
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 for tap.db and reverse-lookup.sqlite (default /keytrace-data in the image)TAP_BIN — path to the tap binary (default tap, on $PATH in the image)TAP_HOST / TAP_PORT — where tap should be reached (defaults 127.0.0.1 / 2480)KEYTRACE_SERVER_ENTRY — override the Nitro server entry path (defaults to the built apps/keytrace.dev/.output/server/index.mjs)The runner package implements a recipe-based verification system:
http-get, dns-txt, css-select, json-path, regex-matchExample 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
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 structureUse 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:
@keytrace/runner, @keytrace/claims, and @keytrace/lexiconAfter running, push to remote:
git push && git push --tags
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.
Every service provider follows the same pattern:
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.
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 the ServiceProvider interface:
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 casesRegister it in packages/runner/src/serviceProviders/index.ts
Icon: Set ui.icon to 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 to apps/keytrace.dev/components/icons/ and register it in the iconMap in apps/keytrace.dev/composables/useServiceRegistry.ts. Set ui.iconDisplay: "raw" for standalone SVGs (like npm/tangled) that shouldn't be wrapped in a circular badge.
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:
packages/runner/src/serviceProviders/ServiceProvider interface (URI regex, processURI, ui config, getProofText, test cases)packages/runner/src/serviceProviders/index.tsWhen you open your PR, please include:
http, dns, and activitypub. If your service needs something different, note thatMIT
TypeScript
66.1%
Vue
32.2%
The monorepo for a keybase on atproto
See the codeIdentity verification for ATProto. Link your decentralized identity (DID) to external accounts like GitHub, LinkedIn, Instagram, DNS, and Mastodon with cryptographically signed attestations.
Keytrace allows Bluesky users to prove ownership of external accounts by:
dev.keytrace.claim recordClaims are user-owned, portable, and stored directly in your ATProto repository.
keytrace/
├── apps/
│ └── keytrace.dev/ # Nuxt 3 web application
├── packages/
│ ├── runner/ # Core verification library (@keytrace/runner)
│ └── lexicon/ # ATProto lexicon schemas
# Install dependencies
yarn install
# Start development server
yarn dev
# Run tests
yarn test
# Type checking
yarn typecheck
# Format code
yarn format
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
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 for tap.db and reverse-lookup.sqlite (default /keytrace-data in the image)TAP_BIN — path to the tap binary (default tap, on $PATH in the image)TAP_HOST / TAP_PORT — where tap should be reached (defaults 127.0.0.1 / 2480)KEYTRACE_SERVER_ENTRY — override the Nitro server entry path (defaults to the built apps/keytrace.dev/.output/server/index.mjs)The runner package implements a recipe-based verification system:
http-get, dns-txt, css-select, json-path, regex-matchExample 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
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 structureUse 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:
@keytrace/runner, @keytrace/claims, and @keytrace/lexiconAfter running, push to remote:
git push && git push --tags
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.
Every service provider follows the same pattern:
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.
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 the ServiceProvider interface:
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 casesRegister it in packages/runner/src/serviceProviders/index.ts
Icon: Set ui.icon to 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 to apps/keytrace.dev/components/icons/ and register it in the iconMap in apps/keytrace.dev/composables/useServiceRegistry.ts. Set ui.iconDisplay: "raw" for standalone SVGs (like npm/tangled) that shouldn't be wrapped in a circular badge.
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:
packages/runner/src/serviceProviders/ServiceProvider interface (URI regex, processURI, ui config, getProofText, test cases)packages/runner/src/serviceProviders/index.tsWhen you open your PR, please include:
http, dns, and activitypub. If your service needs something different, note thatMIT
TypeScript
66.1%
Vue
32.2%