One identity,
many proofs.
Keytrace links your atproto handle to the accounts you already own — GitHub, DNS, npm, Mastodon, PGP — with signed, portable proofs. You be you, everywhere.
A proof is a link you control, connecting your atproto identity.
Sign in with your internet handle
OAuth with your atproto PDS. No new account. No PGP keys to generate.
Post a proof where you already are
Drop a short token into a gist, a DNS TXT record, a package README, a pinned post.
Keytrace signs the claim
Our runner verifies the proof and writes a dev.keytrace.claim record to your repo.
Fresh proofs from the network.
Every claim here was written to its author's PDS. Stored in their repo, verifiable by anyone.
Same lexicon. Same libraries.
Your identity layer.
Keytrace is built on dev.keytrace.* — an open atproto lexicon. Anything Keytrace can read, your app can read. Everything we verify, you can re-verify. Nothing is locked behind our API.
{
"lexicon": 1,
"id": "dev.keytrace.claim",
"defs": {
"main": {
"type": "record",
"key": "tid",
"description": "An identity claim linking this DID to an external account",
"record": {
"type": "object",
"required": [
"type",
"claimUri",
"identity",
"sigs",
"createdAt"
],
"properties": {
"type": {
"type": "string",
"knownValues": [
"github",
"dns",
"activitypub",
"bsky",
"npm",
"tangled",
"pgp",
"twitter",
"linkedin",
"instagram",
"reddit",
"hackernews",
"orcid",
"itchio",
"discord",
"steam"
],
"description": "The claim type identifier"
},
"claimUri": {
"type": "string",
"description": "The identity claim URI (e.g., for github: https://gist.github.com/username/id, dns:example.com)"
},
"identity": {
"type": "ref",
"ref": "#identity",
"description": "Structured data about the claimed identity, derived from the claimUri and knowledge inside the verification process. This is not the raw claim data, but a normalized set of fields that can be used for display and more."
},
"sigs": {
"type": "array",
"items": {
"type": "ref",
"ref": "dev.keytrace.signature#main"
},
"description": "Cryptographic attestation signatures from verification services, for example from @keytrace.dev. These are optional, and so you can pass an empty array. Keytrace will place its attestation signature in the claim here so that the library @keytrace/claims can be used by external developers to not have to implement their own verification logic."
},
"comment": {
"type": "string",
"maxLength": 256,
"description": "Optional user-provided label for this claim"
},
"status": {
"type": "string",
"description": "Current verification status of this claim. Absent on legacy records, treated as 'verified'.",
"knownValues": [
"verified",
"failed",
"retracted"
]
},
"lastVerifiedAt": {
"type": "string",
"format": "datetime",
"description": "Timestamp of the most recent successful re-verification by the system"
},
"failedAt": {
"type": "string",
"format": "datetime",
"description": "Timestamp when the claim last failed re-verification or was retracted"
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Datetime when this claim was created (ISO 8601)."
},
"nonce": {
"type": "string",
"maxGraphemes": 128,
"description": "Random one-time value embedded in the challenge text posted to the external service. Used by verifiers to confirm the proof was created specifically for this claim session. Keytrace itself does not use this, but other services making claims may require it for their verification process."
},
"prerelease": {
"type": "boolean",
"description": "Whether this claim was created during the prerelease/alpha period"
},
"retractedAt": {
"type": "string",
"format": "datetime",
"description": "Datetime when this claim was retracted. Present only if the claim has been retracted (ISO 8601)."
}
}
}
},
"identity": {
"type": "object",
"description": "Generic identity data for the claimed account",
"required": [
"subject"
],
"properties": {
"subject": {
"type": "string",
"description": "Primary identifier (username, domain, handle, etc.)"
},
"avatarUrl": {
"type": "string",
"format": "uri",
"description": "Avatar/profile image URL"
},
"profileUrl": {
"type": "string",
"format": "uri",
"description": "Profile page URL"
},
"displayName": {
"type": "string",
"description": "Display name if different from subject"
}
}
}
}
} Keep the handle.
Prove the rest.
Two minutes, one proof, one signed claim in your repo.