# RealHandles, full documentation corpus Every page below is sourced from https://docs.realhandles.com. Pages are separated by `---` and prefixed with their canonical URL. --- # Claiming an account URL: https://docs.realhandles.com/guides/claiming-an-account/ Description: Assert an account you cannot verify, and what a claim is worth to someone reading it. A claim is your signed word that an account is yours. Nothing checks it. It shows as **Claimed** on your profile, kept in its own section so a reader can tell at a glance what was checked and what was not. ## What a claim does and does not do | | Claim | Verified account | | --- | --- | --- | | Shown on your profile | Yes, under Claimed | Yes, under Verified | | Goes into your signed manifest | Yes, once you publish | Yes, once you publish | | Counts toward your trust score | No | Yes | | Can hold a short handle | Never | On a key platform or a domain | Scarce handles are backed by something you actually proved, so no number of claims takes one from somebody who can prove it. ## When a claim is the right answer Use one when the platform gives no way to prove anything: no OAuth app, no readable page, no post the platform will confirm the author of. That is most of the long tail. A claim is not worthless in that situation, but its value is borrowed. It is believable in proportion to everything around it. An identity with a verified domain and four verified platform accounts saying "this Goodreads profile is also me" is credible. The same sentence on an identity with nothing verified is just a sentence. ## Upgrading a claim later Nothing is stuck. If a platform becomes verifiable, [verify](/guides/verifying-an-account/) the same account and the row moves from Claimed to Verified. What was already published stays published, because the history is append-only, and the next version you sign carries the new status. ## It is still a signed statement A claim is not a note in our database. Once you publish, it is inside a document signed by your key, which means you are on the record as having said it. That is the honest trade: nothing checked it, but you cannot later pretend you did not say it. ## Publish for it to count Adding a claim changes `realhandles.com` straight away and changes your signed proof only when you publish. Until then no key has vouched for it, so a reader has only our word for it. See [The signed manifest](/concepts/the-signed-manifest/). ## In the Android app Claims live under **Identity > Claim**, kept in a separate section from Verify for the same reason the public profile keeps the two lists apart. Adding one starts where a verification starts: **Add an account**, paste the link. If nothing can confirm that link the app says so and offers to record it as Claimed. If something can confirm it, the stronger route is offered first, which is the right default. Either way you see which one you are getting before you commit to it. You can also enter a claim by hand, with the platform, the handle, and an optional profile link. Each row has arrows to step it up or down, and a third control that opens To top, To bottom, and a box to type a position, so moving a row from eighty to three is one action rather than seventy-seven taps. Moves are held until you save them, and the order you set here is the order the manifest publishes. --- # Disavowals URL: https://docs.realhandles.com/guides/disavowals/ Description: Say publicly that an account is not you, in a way anyone can check. A disavowal is the inverse of a claim: a signed statement that an account is **not** you, or that you are not on a platform at all. It sits in the same signed manifest as everything else, so it carries the same weight as the accounts you claim. ## Two kinds **This account is not me.** Names one specific profile, by handle or by link. Use it for a known impersonator, or for a same-name person who is not doing anything wrong but keeps getting mistaken for you. **I am not on this platform.** Declares that you have no account there at all, which makes every account on that platform claiming to be you fake by definition. Stronger than listing impersonators one at a time, and it does not go stale as new ones appear. ## What you can say A disavowal needs a platform. An account disavowal also needs a handle or a link, because a statement that does not say which account is not a statement about anything. A short note is optional, and it is the part that does the work for a reader: "known impersonator", or "same name, different person". Order matters. The list is shown in the order you set, so put the one people keep falling for at the top. ## Publish for it to count The statement lands on `realhandles.com` immediately and enters your signed proof when you publish and re-sign. Until then, anyone verifying you independently sees the previous version with no disavowal in it. See [The signed manifest](/concepts/the-signed-manifest/). Editing or removing one changes what you publish from now on. Versions you already signed keep what they said, because the history is append-only, and that is what makes editing an old version impossible for us as well as for you. ## Reporting somebody else's impersonator Disavowals are what **you** say about accounts elsewhere. Reporting is what anybody says about a RealHandles profile, and it needs no account at all, because the person being impersonated usually does not have one. A human reads reports. This is the one thing in the product you do about someone else, so it is deliberately kept off the signed manifest. Your key says what is yours and what is not yours. It does not adjudicate other people. ## What it looks like ![The disavow panel, for stating publicly that an account is not you](../../../assets/disavow.png) A disavowal is signed into the manifest exactly like a verified account, so it carries the same weight as the rest of the identity rather than being a note on the side. ## In the Android app Disavowals get a section of their own: **Identity > Disavow**. Adding one asks the same first question the website does, as two choices at the top of the form: **One account**, which names a single profile by handle or link, or **A whole platform**, which says you have no account there at all. Everything after that is the platform, the handle or link, and the optional note that does the real work for a reader. Published statements reorder with the same arrows and jump-to-position control as accounts, and the order you set is the order the manifest publishes, so the impersonator people keep falling for goes at the top. Correcting one that is already published is on the row itself, under Options. What a statement covers cannot be edited there: a disavowal of one account never becomes a disavowal of the whole platform by an edit, because those are different statements. Remove the old one and add the one you meant. Reporting somebody else's impersonator is deliberately somewhere else in the app, under **Identity > Data**, and needs no identity of your own. --- # Exporting your data URL: https://docs.realhandles.com/guides/exporting-your-data/ Description: Take the whole identity with you: manifest, chain, proofs, and timestamps. Everything that matters about your identity is a file you can download right now, and the important ones are public, so you do not even need to be signed in to fetch them. ## The files | File | Where | What it is for | | --- | --- | --- | | `realhandles.json` | `realhandles.com//realhandles.json` | Your current signed manifest. The proof itself. | | `realhandles-chain.json` | `realhandles.com//realhandles-chain.json` | Every version ever published, oldest first. | | `realhandles.ots` | `realhandles.com//realhandles.ots` | The OpenTimestamps proof that the current version existed by a given Bitcoin block. | | `did.json` | `realhandles.com//did.json` | A W3C DID document, so the profile resolves as a `did:web` for standard DID tooling. | | `realhandles-.json` | The Export action in your dashboard | Our full copy: handle, profile, accounts, anchor, and the signed manifest. | | `realhandles-key.json` | Downloaded when you make or re-wrap your key | Your encrypted key backup. | ## What each one is actually for **`realhandles.json` is the one that survives us.** It is a compact JWS signed by your key. Anyone can check it against your public key with off-the-shelf tooling and no access to RealHandles. If this site disappeared tomorrow, this file still proves what it proves. **`realhandles-chain.json` is the audit trail.** The current manifest tells you what is true now. The chain tells you how it got that way, including any key change and whether it was authorized. See [Reading the history](/concepts/reading-the-history/). **The dashboard export is the directory copy.** It lists what we hold: your handle, your profile, your accounts, your anchor, and the signed manifest. Useful as a single snapshot, and honest about the split: the signed manifest inside it verifies independently, and the rest is our record. **`realhandles-key.json` is not an export of your identity.** It is an encrypted copy of your signing key, and it is the only file on this page that must stay private. See [Your signing key](/guides/your-signing-key/). :::caution Your private key is not in any export, and it never can be. We do not have it. ::: ## Verifying an export yourself ```ts import { verifySignedManifest } from '@realhandles/verify'; const file = await fetch('https://realhandles.com/david/realhandles.json').then((r) => r.json()); const { valid, keyId, manifest } = await verifySignedManifest(file); ``` `@realhandles/verify` is the same verification code this site runs, published on npm and open source. Using it is convenient rather than required: the format is a standard EdDSA JWS, so any JOSE library gets the same answer. ## Importing what you already have The other direction works too. Point the importer at a page you control, or upload a file, and it pulls out profiles from `rel="me"` links, `schema.org` `sameAs`, or a plain JSON link list, then marks the ones already attached to you. It is a shortcut for entering accounts, not a proof. Each one still has to be verified or claimed. See [Verifying an account](/guides/verifying-an-account/). ## Leaving Deleting the identity takes it off our files. Everything you already published stays published: copies people downloaded, the anchor file on your own domain, and the Bitcoin timestamps, none of which we can recall. Export first if you want a copy of the directory record. ## Where these live ![Everything held about you in one file](../../../assets/data-export.png) Everything held about you in one file. The signed manifest inside it verifies without RealHandles, which is the point of the export. ## In the Android app **Identity > Data > Export everything** produces the same file the dashboard export does. Before it hands anything over the screen shows what is in it: the handle, the number of accounts, how many signed versions exist, the key that signed the current one, and the first few lines of the JSON. If nothing has been published yet it says that outright, because a directory copy with no signature in it is worth knowing about before you file it away. Two ways out of the app. **Save it on this phone** writes the file. **Share my export** hands it to another app, behind a confirmation, because the share sheet puts a cloud drive and a group chat one tap from a file naming every account on your identity. Past 256 KB the share route is withheld and only saving is offered. Android moves an app-to-app payload through a single one megabyte buffer shared by everything the app has in flight, so a payload may safely use only a fraction of it; going over throws in the sending app and the share sheet simply never appears. The public files stay public and need no app at all. `realhandles.json`, `realhandles-chain.json`, `realhandles.ots` and `did.json` are URLs, fetchable from anything. Two things on this page have no app equivalent. The importer that reads `rel="me"` links off a page is website only. So is `realhandles-key.json` as an export of your key, which the app treats as a key transfer rather than as data, under **Keys > Send key to another device**. See [Your signing key](/guides/your-signing-key/). --- # Getting started URL: https://docs.realhandles.com/guides/getting-started/ Description: Create an identity, generate a signing key, and publish your first signed manifest. At the end of setup you have three things: a handle at `realhandles.com/`, an Ed25519 signing key that only you hold, and a signed file that ties the two together and can be checked by anyone, with no help from us. ## Do it in this order Verify at least one account **before** you pick a handle. The handle gate reads the proofs already attached to your login, so a handle of 8 characters or fewer is only offered once you hold a matching proof. Proofs are stored against the login, not against an identity, which is exactly why you can verify before you have claimed anything. If you would rather not decide yet, onboarding fills in a long default handle for you. Long handles are open to anyone, so nothing is at stake in taking one, and you can move to a short one later once you have proved it. ## The five steps 1. **Sign in.** This answers one question: which identity row you are allowed to edit. It never signs anything on your behalf. See [Your signing key](/guides/your-signing-key/). 2. **Verify an account.** GitHub, X, YouTube, Bluesky, TikTok, a domain, a wallet, a Nostr key. See [Verifying an account](/guides/verifying-an-account/). 3. **Pick a handle.** The tier is decided by length. See the table below. 4. **Make your key.** It is generated in your browser. You choose a backup passphrase and download an encrypted backup file. 5. **Publish.** Your key signs a manifest listing your accounts, and that signed file becomes your public proof. ## Which handles you can take | Length | Tier | What it takes | | --- | --- | --- | | 9 or more | Open | First come, first served. A matching proof adds a namesake badge. | | 3 to 8 | Protected | One matching verified account on a key platform, or a matching domain you control. | | 1 to 2 | Reserved | A strong match: a matching `.com`, or a matching handle on X, YouTube, TikTok, Instagram, or LinkedIn. | A match means the proof carries the same name as the handle: `@ben` on X for `/ben`, or `ben.com` for `/ben`. Cheap TLDs score below the protected threshold on purpose, so nobody takes a scarce name by spending a dollar on a domain. The scarcity is borrowed from the source platform: `@ben` on X is globally unique and hard to get, which is what makes holding it a real claim to the name. ![The RealHandles dashboard with the key unlocked, showing the sign button and the Accounts, Profile, Disavow, Publish and Data tabs](../../../assets/dashboard.png) The dashboard once the key is unlocked. Everything you can change lives under one of those five tabs, and the sign button at the top is what makes any of it public. ## Save exactly two things - **Your encrypted key backup, plus the passphrase.** The file is useless without the passphrase, and the passphrase is useless without the file. Keep the file somewhere that is not the device you use every day. - **Your recovery code.** It is a key you designate in advance, published inside your manifest, and it is the only way back in if the signing key is gone. See [Recovery and rotation](/guides/recovery-and-rotation/). :::caution There is no reset link. A reset link would mean somebody other than you, including us, could sign as you. That is the failure mode this whole design exists to avoid. ::: ## The part that surprises everyone Changing something on RealHandles does not change your proof. An added account, a new anchor, a reordered list: all of it lands on `realhandles.com` immediately, and reaches your signed proof only when you publish again and your key signs the new version. Until you do, anyone verifying you sees the previous version, which is the correct answer to give. [The signed manifest](/concepts/the-signed-manifest/) explains why it works that way. ## What you end up with ![A published RealHandles profile, showing verified accounts above claimed ones](../../../assets/public-profile.png) Verified accounts sit above claimed ones, under separate headings, with the badges that say what has been checked: how many accounts are verified, the trust score, whether the handle is a namesake, whether the history is anchored in Bitcoin, and how many signed versions exist. ## In the Android app The app runs the whole of setup, and everything else on this page. What it does not run is [rotation and recovery](/guides/recovery-and-rotation/), which stay on the website. Four tabs along the bottom: | Tab | What it is | | --- | --- | | Identity | Your own identity, and everything you change about it | | Check | Checking somebody else's handle | | Directory | Published identities, searchable | | Keys | The signing key on this phone | **Check is about someone else.** Proving your own accounts is the Verify section inside Identity. Those two words sound alike and do opposite jobs, which is why the app keeps them on separate tabs. Identity is split into five sections: | Section | What lives there | | --- | --- | | Verify | Proving an account is yours, and the order the proved ones appear in | | Claim | Accounts you say are yours, with nothing checking it | | Disavow | Signed "not me" statements | | Profile | Display name, photo, person or organization, and the handle | | Data | Anchor, signed history, export, reporting a profile, and deleting the identity | ### Setting up on the phone **Claim your handle**, on the Identity tab, is the same five steps in the same order. Pick the name, the phone generates the key, you set a backup passphrase and save the encrypted backup somewhere that is not the phone, and only then is the handle claimed and the public key registered. The private half never leaves the device. If you already have an identity, do not make a second key. Move the one you have: on realhandles.com open the Profile tab, choose Show as QR code, and scan it from the Keys tab. See [Your signing key](/guides/your-signing-key/). Publishing is **Publish & re-sign** on the Identity tab, and it needs the key on the phone and unlocked, which is the same rule the website follows. ## Where to go next - [Verifying an account](/guides/verifying-an-account/) for a stronger handle or a higher trust score. - [Claiming an account](/guides/claiming-an-account/) for the platforms that cannot be verified at all. - [Disavowals](/guides/disavowals/) if somebody is already impersonating you. - [Your signing key](/guides/your-signing-key/) for what you are actually holding. - [Reading the history](/concepts/reading-the-history/) for how to audit an identity, including somebody else's. - [Exporting your data](/guides/exporting-your-data/) for every file you can take with you. --- # Recovery and rotation URL: https://docs.realhandles.com/guides/recovery-and-rotation/ Description: Replace a key you still hold, and take back an identity whose key is gone. Two different problems, two different answers. - **Rotation** is for a key you still have and no longer want. The old key signs the handoff. - **Recovery** is for a key that is gone. Keys you named in advance sign the handoff instead. Both change the signing key. Neither one involves a login, a support ticket, or us. There is no third path, and adding one would mean somebody other than you could sign as you. ## Rotation You generate a new key, and your **old** key signs a statement authorizing exactly that new key at exactly that point in your history. The new manifest carries both: it is signed by the new key, and it contains the old key's authorization. That is what makes it a handoff rather than a stranger appearing. Anyone who had pinned the old key sees an authorization the old key produced, and follows it forward. Somebody who has only stolen the new key cannot produce that authorization. The server checks four things before it accepts the change: 1. The manifest verifies under the key that signed it. 2. Its position in your history matches the one we actually hold. The authorization is bound to a position, so it cannot be saved up and replayed later. 3. The named previous key is the one currently registered to you. 4. The old key's signature covers exactly this key change, byte for byte. ### Two things break on purpose when you rotate **Your anchor drops back to unchecked.** The file hosted on your domain was signed by the retired key, so it is no longer proof of anything. Re-host and re-verify. Leaving a green badge on a file that no longer verifies would be a lie. See [Anchoring](/concepts/anchoring/). **Your old encrypted backup stops unlocking this identity.** It still opens, and it still contains the old key, which is now the wrong key. Take a fresh backup, which the flow requires before it lets you finish. ## Recovery A recovery key is a key you designate **in advance**, inside your published manifest, and it is the only thing that can take an identity back when the signing key is gone. Signup asks you to save exactly one thing for this: your recovery code. Set up later and it is still the same mechanism, just later. The policy has a threshold. One printed recovery code is a threshold of one. Recovery contacts, meaning other people's keys, are the same shape with a higher threshold, which is why this is a policy rather than a single field. :::caution A designated recovery key is as powerful as your signing key. Whoever holds enough of them can take the identity. The threshold is the only dial that makes that harder, so treat a printed recovery code exactly like the key itself. ::: ### The rule that makes recovery safe The policy that counts is the one published **before** the recovery, never the one the recovering entry declares about itself. Otherwise an attacker could simply arrive naming a fresh set of recovery keys and approve the takeover with them. Every time you re-sign, the current policy is carried forward automatically, so an ordinary publish never quietly strips a recovery key you set up months ago. If you never published a policy, there is nothing to recover with, and a lost key is final. That is the honest consequence of having no reset link. ### Recovery contacts The threshold mechanism is built and tested, so "three of my five friends can bring me back" already works at the protocol level. What is missing is the interface for inviting people and collecting the keys. Until that ships, use a printed recovery code. ## What this actually guarantees **Guaranteed.** Anyone reading your full history can tell an authorized key change from an unauthorized one. Both a rotation and a recovery leave a signed record inside the chain, and neither can be forged by somebody holding only the new key. We cannot forge one either, because we never hold a key that can sign the statement. **Not guaranteed.** A verifier who has only ever seen your latest manifest pins whatever key signed it, and cannot tell whether that key arrived legitimately. Catching an unauthorized swap requires the history, which is why every version is kept and served. See [Reading the history](/concepts/reading-the-history/). Also not guaranteed: that a recovery key stays in the right hands. The protocol enforces that the holder was designated by you. It cannot know who is holding it now. ## In the Android app Neither one is in the app. This is the page with the biggest gap between the two, and the Keys tab says so rather than hiding it: a **Rotation & recovery** button there opens realhandles.com, and the reason is one line under it. Both actions turn on a signature the app is not in a position to produce cleanly. A rotation needs the outgoing key to sign the handoff at the moment a new key is being made, and a recovery needs keys designated in advance, which are by definition not the key sitting on the phone you are holding. Doing either badly is how somebody ends up locked out of an identity for good, so the app sends you to the website instead of offering a shortcut. What the app does show is the result. The signed history under **Identity > Data** names a rotation or a recovery on the version that carried it, and says whether the handoff was authorized. See [Reading the history](/concepts/reading-the-history/). --- # Verifying an account URL: https://docs.realhandles.com/guides/verifying-an-account/ Description: Prove you control an account on another platform, and what counts as a real proof. An account is **Verified** when a first party confirmed you control it: the platform itself, a post only the account holder could have made, a DNS record, or a signature from the key behind the account. Everything else is **Claimed**, which is your word and nothing more. The distinction is not decoration. If the two were mixed together the word verified would stop meaning anything, which would make the whole directory worthless. ![The same profile with a Verified accounts section above a Claimed accounts section, each with its own explanation](../../../assets/public-profile.png) The two lists are drawn apart on purpose, and the claimed one says plainly that RealHandles has not confirmed anything about it. ## Verify before you claim a handle Proofs attach to your login, not to an identity. That is deliberate: a handle of 8 characters or fewer needs a matching proof, and you would otherwise have to claim the handle before you could prove you deserved it. Verify first, then claim. ## The methods | Method | What it proves | Status | Can hold a short handle | | --- | --- | --- | --- | | Sign in with the platform (OAuth) | The platform says this account is yours | Verified | Yes, on a key platform | | Post a one-time token (X, TikTok) | Only the account holder could publish it, and the platform tells us the author | Verified | Yes | | Token in your channel description (YouTube) | Same, read back through the platform's own API | Verified | Yes | | Bluesky handle lookup (atproto) | The public atproto record resolves to you | Verified | Yes | | Domain control (file or DNS TXT) | You control the domain | Verified | Yes, and it is the strongest | | Wallet signature | The address signed our challenge | Verified | No | | Nostr signature | The key behind the account signed our challenge | Verified | No | | Mutual `rel="me"` link | You listed the page and the page links back to this exact profile | Verified | No, see below | | Token somewhere on a page | Somebody edited that page | Claimed | No | | Self-attested | Nothing | Claimed | No | Live OAuth platforms today: GitHub, Discord, Twitch, GitLab, Dribbble. Instagram and LinkedIn have no reliable read path yet, so an account there lands as [Claimed](/guides/claiming-an-account/) until an OAuth app exists. ![Three proofs, three different methods](../../../assets/verified-list.png) Three proofs, three different methods. A domain verified by DNS sits in the same list as an OAuth sign-in and a posted token, because all three are first-party checks. ## Why some verified proofs still cannot hold a short handle Three separate questions get asked about every proof, and a proof can pass one and fail the next. 1. **Is it verified?** Did a first party confirm control. 2. **Does it add to the trust score?** Every verified proof does. Claims do not. 3. **Can it reserve a scarce handle?** Only a domain, or a matching handle on a curated key platform, proved by a method other than `rel="me"`. The third bar is the strict one, and the reason is scarcity, not strength of evidence. `@ben` on X is globally unique and hard to get, so holding it really is a claim to the name "ben". A `rel="me"` link on a page you built, or an account on a platform where anyone can register any name, proves you control that thing but proves nothing about the name. A namesake could mint one just as easily as you can, so it never takes a scarce name from somebody who can prove it. Same logic for domains. A matching `.com` scores highest, established TLDs score below it, and anything else scores under the protected threshold. Otherwise `ben.xyz` for a dollar would beat a person who actually holds `@ben`. :::note A proof that cannot hold a short handle is still worth having. It shows Verified on your profile and it counts toward your trust score, which is what makes the rest of your identity believable. ::: ## Signing in with the platform (OAuth) ![The one-click OAuth row: GitHub, Discord, Twitch, Dribbble and GitLab](../../../assets/oauth-buttons.png) The strongest and fastest proof, because the platform itself answers. Pick one and it sends you there and back. ![The Discord consent screen, listing what RealHandles can and cannot see](../../../assets/discord-consent.png) The platform asks before it tells us anything. RealHandles requests your username and which servers you are in, and the screen states outright that it cannot read or send your messages. ![The verification success message, saying the proof is saved but not yet published](../../../assets/oauth-success.png) Verified the moment you land back, and not yet public. That second half is the part people miss. If you administer servers or organizations on that platform, you are offered them next: ![The Discord server picker, asking for a permanent invite link](../../../assets/discord-server-picker.png) Being an admin proves control regardless of who signed in, so these can be verified as first-party accounts too. Discord has no public server URL, so a permanent invite link stands in for one. One thing to expect: if the desktop app is installed, the platform may try to hand the login to it and the browser tab will look stuck. Use the link to continue in the browser, or quit the app. ## Domain proofs ![The domain verification step, showing the challenge token to publish](../../../assets/domain-challenge.png) The token can go in a DNS TXT record on the domain, or in a file at `/.well-known/realhandles-challenge.txt`. It stays valid while DNS propagates, so there is no race to win. Publish a one-time token at `/.well-known/realhandles-challenge.txt`, or add it as a DNS TXT record. Either one on its own is enough. The token is yours and does not expire, so setting it up on a computer later is fine. A check that fails right after you set it up is normal. DNS changes and caches take minutes to hours to reach us. Nothing is lost, so wait and check again. A verified domain is separate from an [anchor](/concepts/anchoring/), which is where a copy of your signed identity is hosted. Proving `ben.com` and anchoring at `ben.com` are two different actions. ## Wallets and Nostr keys Both accounts are keypairs already, so a signature over our challenge settles it outright. No platform is asked and no page is read. - The challenge is tied to your login and your key, so a signature made for it cannot be replayed somewhere else. - The server recovers which address or public key signed. That address, not one you typed, is what gets recorded. - A signature is public. Producing one proves the wallet without opening it, and no transaction is made. If the address has a primary ENS name, or the Nostr key has a confirmed NIP-05, your profile publishes the readable name instead of the raw string. The key underneath is still what the identity rests on. ## After you verify, publish A new proof lands on `realhandles.com` immediately and reaches your signed manifest only when you publish and re-sign. Until then, anyone verifying you independently sees the previous version. See [The signed manifest](/concepts/the-signed-manifest/). ## Hiding an account Hiding keeps the account and everything it has already proved. It stays attached to your identity and it still counts toward the handles you are allowed to hold. What changes is that the next version you publish leaves it out of the public manifest. So hiding on its own changes nothing for anyone checking you: it comes off your public proof when you publish, and showing it again later is a publish of its own, not an undo. ## In the Android app Verifying lives under **Identity > Verify**. The methods are the same ones in the table above, reached differently: one way in, and three exceptions. **Add an account** is the one way in. Paste the link to the account and the server answers with everything that link supports, strongest first. Paste `github.com/yourname` and you are offered the GitHub sign-in. Paste a page nothing can confirm and you are offered a [claim](/guides/claiming-an-account/) instead. Before anything happens the screen says which one you are about to get, and whether that proof is strong enough to reserve a short handle. The three rows under it are the cases a link cannot reach: - **Verify a domain.** Name the domain, take the token, publish it in a file or a DNS TXT record, then press Check now. The token does not expire, so finishing on a computer later is fine. - **Verify a Nostr key.** Signing is handed to a NIP-55 signer app such as Amber. The secret key stays inside the signer and is never seen by RealHandles or by the app. - **Verify a wallet.** The app shows the exact message, you sign it in the wallet app and paste the signature back. No transaction, no gas. Posting a token works the way it does on the website: the app issues the token, you publish it, then paste the link to the post and run the check. ### OAuth on the phone Signing in with the platform works in the app. The app asks realhandles.com for a start URL, opens it in the phone's browser, and the finished handshake comes back to the app on a deep link. Two things about that return are worth knowing, because they explain what you see: - Nothing the return link says is displayed. The wording names the platform **the app** started, not whatever the link carried. - What happened is read back from realhandles.com afterwards, because the server is the only party that knows whether a proof was actually created. If you administer organizations or servers on that platform, the app offers them next, the same as the website does. A Discord server needs a permanent invite link, because Discord has no public server URL. If the copy of realhandles.com being talked to cannot hand the result back to the app, the app refuses to start rather than strand you in a browser tab, and says to use the website for that one. ### Hiding, on the phone The app can hide an account and cannot show it again. A hidden account is left out of the list the app reads, so once you leave the screen there is no row left to tap. Showing it again is done on realhandles.com. --- # Your signing key URL: https://docs.realhandles.com/guides/your-signing-key/ Description: The Ed25519 key that is the root of trust, where it lives, and how to back it up. Your Ed25519 signing key is the identity. Not your login, not your handle, not your domain. It is generated in your browser, it never reaches our servers, and everything public about you is a document that key signed. ## Why the key and not the login Login answers one question: which identity row you are allowed to edit. It cannot sign, so it cannot produce a proof, so nobody who steals a session can become you. That boundary is the point of the product. Merge login authority with proof authority and you get a service where the operator can sign as any user, which means every proof is really a statement about the operator instead of about the person. Keeping them apart is what makes the proof survive us. The same logic covers the destructive actions. Renaming a handle and deleting an identity never touch the manifest, so a stolen session could once vandalise an identity it could not forge. Both now need a signature from the identity's key as well as a login. ## Why the key and not the domain A domain is a lease. It expires, it gets seized, it gets bought by somebody else. If a domain were the identity, whoever holds it next would inherit you. Under this design the next holder inherits a name and nothing else. A file can still be hosted at that address, but it cannot carry your signature, so verification fails and the identity does not move. See [Anchoring](/concepts/anchoring/). ## Where the key actually lives | Copy | Where | Protected by | | --- | --- | --- | | Working copy | In the page, and in this browser's IndexedDB for 7 days after you unlock | Non-extractable: it can sign while the app is open, and the raw bytes cannot be read back out | | Encrypted backup | This browser's local storage, and the file you downloaded | Your passphrase, via PBKDF2 (210,000 iterations) into AES-GCM | | Passkey unlock, optional | This device's authenticator | Face ID, fingerprint, or device unlock | We hold the public half. That is all we need to check a signature, and all anyone needs to check yours. ![The RealHandles dashboard with the signing key locked, offering a backup file, a passphrase, and a separate recovery key path](../../../assets/key-locked.png) Signed in and still unable to sign. The session knows who you are and holds no key, so the profile is readable and the proof is not editable. That is the whole model in one screen. ## Back it up properly Download the encrypted backup and put it somewhere that is not the device you use every day. Losing the device is the exact situation the backup exists for. Your passphrase is the only thing that opens it. It is not stored anywhere and it cannot be reset, so keep it in your password manager, not in your head. :::caution If you lose both the backup and the passphrase, and you have no recovery key, you are locked out. This works like self-custody, and it works that way on purpose: an identity nobody else can sign for is also an identity nobody else can steal. Set up a [recovery key](/guides/recovery-and-rotation/) so that "both are gone" is not the end of the story. ::: ## Passkeys A passkey saves you typing the passphrase every time. It does not replace the passphrase. - **With PRF support** (Apple, Windows Hello, Android, recent 1Password and Bitwarden) the authenticator derives the unlock secret itself, and it exists only for the moment of unlock. - **Without PRF**, the key is wrapped with a random secret held in this browser and a successful passkey check is required before anything decrypts. That is a convenience on this device, not a hardware-bound secret. Either way the passphrase backup stays the strong path, so no authenticator can ever lock you out. Exporting a backup or changing a passkey still needs a fresh passphrase unlock, because those operations need the raw key. ## Moving to another device Move the encrypted backup, not the key. The file is already encrypted, so the passphrase stays in your head and the file can travel by any route you like. On the other device, import it and unlock it. The QR transfer in the mobile app does the same thing: the code carries the encrypted backup, and the passphrase is still needed to open it. A code has a size limit of about 2.9 KB, so use the file if the transfer refuses. ## Changing the passphrase Changing it re-wraps the copy you are working with. Your key, your handle, and your published history are all untouched, because none of them depend on the passphrase. What does not change: backup files you already downloaded, and any QR code or printout made from one. Those stay encrypted under the old passphrase, and they keep working with it forever. Export a fresh backup if you want everything on the new one. ## In the Android app Everything about the key is on the **Keys** tab: which key this phone holds, its key ID and `did:key`, whether it is unlocked, and the keys this device has pinned for other people. ### Getting the key onto the phone Two ways in, and only one of them is right for an identity that already exists. - **Scan it.** On realhandles.com open the Profile tab, choose Show as QR code, and scan that from the Keys tab. The code carries the encrypted backup, so the passphrase is still needed to open it, and a code tops out around 2.9 KB. Use the file if the transfer refuses. - **Make it here.** Only when you are starting from nothing. Claim your handle on the Identity tab generates the key on the device. Doing this when you already have an identity makes a second, unrelated key, which is not what you want. Sending the key back out is **Send key to another device**, as a QR code or as `realhandles-key.json`. What travels is the encrypted backup, never the key itself, and the app says so before it puts anything on screen or into a share sheet. ### Biometric unlock is not a passkey The website has passkeys. The app has biometric unlock. They do the same job and they are different mechanisms, and neither one travels to the other: setting up a passkey in your browser does nothing for the phone, and turning on biometric unlock does nothing for the browser. Both are per device, and a passkey is per browser on top of that. What biometric unlock actually does: your backup passphrase is kept on the phone, held by hardware that will not release it without your fingerprint or face. Unlocking then works the same as typing the passphrase, because it *is* the passphrase. - Your passphrase never stops working, and it stays the only thing that opens your backup on any other device. - Adding a new fingerprint or face to the phone erases the stored passphrase. That is the design: somebody who enrols a finger on a phone found unlocked gets nothing. Type the passphrase once and turn it back on. - Turning it off destroys the stored passphrase copy and touches neither your key nor your backup. ### Changing the passphrase, and removing the key **Change backup passphrase** re-wraps the copy on that phone and nothing else. The backup you downloaded, and any QR code made from it, still open with the old passphrase. **Remove from this device** takes the key off the phone. If that was the only copy, the identity goes with it, and nobody can restore it, RealHandles included. --- # Anchoring URL: https://docs.realhandles.com/concepts/anchoring/ Description: How a domain anchors an identity without ever becoming the identity. An anchor is a copy of your signed identity, hosted somewhere you control. It is optional, it is one pointer among possible many, and it is never the identity. Your key is. ## What an anchor buys you One thing, and it is a big thing: if RealHandles disappears, the file you hosted is still there and still verifies against your key. Somebody who saved your `keyId` years ago can check it with no working RealHandles and no cooperation from anyone. That is the whole value. An anchor does not make you more verified, it does not raise your trust score, and it does not affect which handle you can hold. ## The two forms **A full manifest** is self-contained. It survives us being gone, which is the point. It needs re-hosting every time you publish a new version, or the copy on your domain goes stale. **A pointer** is signed once and hosted once. It proves you control the location and points at the always-current proof on RealHandles. Publishing again never means touching your server. The trade is that it is not self-contained: if RealHandles is gone, the pointer proves you control the domain but cannot show anyone what you claimed. Either form goes at `/.well-known/realhandles.json` on your domain, or at a raw file URL you control. ## Why a lapsed domain cannot take your identity Every check asks the same question: **is this file signed by the key we already trust?** A domain that lapses, or gets seized, or gets bought by somebody with your name hands over control of an address and nothing to sign with. Any file hosted at that location fails verification, because the signature is not yours and cannot be made to be. So the worst case is that your anchor goes from verified to lapsed. Your key, your handle, your accounts, and your signed history are untouched, and nobody verifying you is misled for a moment. This is the `did:plc` property: follow the key, not the name. The reverse also holds. Moving your anchor to a different domain changes nothing about who you are. It drops back to unchecked until the new location is confirmed, and that is all. ## When an anchor goes back to unchecked - You moved it, or changed where we look. - You [rotated your key](/guides/recovery-and-rotation/). The hosted file was signed by the retired key, so it no longer proves anything about the current one. Re-host and re-verify. - The check ran and the file was missing, unreadable, or signed by a key that is not yours. A check only reads the file. It never writes anything and never changes your identity, so running it as often as you like costs nothing. ## Anchoring is not the same as a domain proof They are easy to confuse because both involve a domain you own. | | Domain proof | Anchor | | --- | --- | --- | | What it proves | You control the domain | A copy of your signed identity lives at a place you control | | How | A one-time token in a file or a DNS TXT record | A signed file hosted at a known path | | Effect on trust score | Yes | None | | Effect on which handle you can hold | Yes, and it is the strongest kind of match | None | | Why you want it | Stronger identity, scarcer handle | Your proof outlives RealHandles | You can do both with the same domain. They are separate actions and either works without the other. See [Verifying an account](/guides/verifying-an-account/) for the proof side. ## Where you set it ![The anchor panel, offering a domain or a GitHub Gist as the host](../../../assets/anchor-panel.png) A pointer file is signed once and hosted once. A full manifest is self-contained and keeps working even if RealHandles is gone, which is the case the anchor exists for. A Gist works when there is no domain to host on. ## In the Android app **Identity > Data > Anchor your identity elsewhere.** The screen offers the same two hosts, a domain or a GitHub Gist, and prefills any domain you have already proved so the name does not have to be retyped. For a Gist it insists on the raw file URL, because the Gist page itself is HTML and a check against that finds no JSON. The status line is the one on this page: anchored, lapsed, or not checked, with when it was last read. **Check anchor** runs it again, and the app repeats what is true, that a check only reads the file and can be run as often as you like. Moving an anchor that currently verifies asks for a confirmation first, and says what actually happens: the location drops back to unchecked until the new one is confirmed, and your key, your handle and your verified accounts are untouched. The app also asks which file you hosted, a pointer or a full manifest. That answer is only so the advice after your next publish is correct: a full manifest needs re-hosting every time, and a pointer never does. --- # Reading the history URL: https://docs.realhandles.com/concepts/reading-the-history/ Description: The sigchain, the OpenTimestamps proof, and how to audit an identity end to end. Every version of an identity that has ever been published is kept, and every version is signed. The chain proves order and authorship. Bitcoin timestamps prove time. Together they mean nobody can rewrite the past quietly, including us. ## The chain Each manifest carries two fields inside the signed payload: - `seq`: its place in line. The first version is 0, and each new one adds 1. - `prev`: `base64url(SHA-256(...))` of the previous version's JWS. Because `prev` is covered by the signature, editing any old version changes its hash, which breaks the link every later version depends on. The history is append-only, and it is append-only as a matter of arithmetic rather than policy. Fetch the whole thing, oldest first: ``` GET https://realhandles.com//realhandles-chain.json ``` ## What a valid chain means `verifyChain` in [`@realhandles/verify`](https://www.npmjs.com/package/@realhandles/verify) checks all of it: 1. Every entry's signature verifies against the public key inside it. 2. `seq` starts at 0 and increments by 1, with no gaps. 3. Each `prev` equals the hash of the entry before it. 4. The signing key stays the same, unless an entry carries a key change that was properly authorized. On a key change it checks the authorization the same way the server did: either the previous key signed the handoff, or enough keys the identity had **already published** as recovery keys signed it. The recovery policy it judges against is the one in force before that entry, never the one the entry declares about itself. See [Recovery and rotation](/guides/recovery-and-rotation/). If you pin a key, pin the **genesis** key. The chain then proves the current key from it, which is exactly the property that lets someone follow you through a rotation without having to be told. ## Reading the outcomes | What you see | What it means | | --- | --- | | Chain valid, same key throughout | Nothing has changed hands. | | Chain valid, key changed with a handoff signed by the old key | An ordinary rotation. Same person, new key. | | Chain valid, key changed with enough designated recovery signatures | A recovery. The identity came back through keys named in advance. | | Key changed with no authorization in the entry | Do not trust it. The chain says so, in the entry itself. | | Key differs from the one you saw before, and no handoff anywhere in the history | The identity you are looking at is not the one you saw before, whatever the handle says. | ## Bitcoin timestamps The chain proves order and authorship, but it does not prove when, and every entry in it lives in our database. An operator with database access could in principle fabricate a whole consistent history and date it to last year. Nothing inside the chain would contradict that. OpenTimestamps closes it. The SHA-256 of each version's JWS is submitted to public calendars and committed into the Bitcoin blockchain. That digest is the same one the next version records as its `prev`, so a timestamp on one entry transitively pins everything before it. Free, and no coins involved. ``` GET https://realhandles.com//realhandles.ots ``` Timestamping runs on a schedule rather than at the moment you publish, because nothing about saving your own signed manifest should depend on a third party being up. The honest consequence: a proof attests "existed no later than this block", a little after you pressed publish. An upper bound on the date is what a timestamp proves anyway. A version showing as pending simply has not made it into a block yet. It does not mean anything is wrong. ## Auditing an identity yourself 1. Fetch `realhandles-chain.json` and run `verifyChain` over it. 2. Compare the resulting key to whatever you pinned last time. If you have never seen this identity before, pin the genesis key now. 3. Fetch `realhandles.ots` and check it with the standard `ots` CLI against the corresponding JWS. Step 3 is the one that matters if you do not want to take our word for anything. What the dashboard shows about Bitcoin status is us reporting on ourselves. The `.ots` file is checkable without us, and we deliberately do not verify the Bitcoin attestation for you: doing that properly needs a block header source, and handing it to your own OpenTimestamps tooling is the more honest answer. ## Finding an identity to read ![The RealHandles directory, listing published identities](../../../assets/directory.png) The directory lists published identities. Every profile it links to serves its own manifest and history at fixed paths, so anything here can be audited by the steps above without asking this site to vouch for it. ## In the Android app Your own history is **Identity > Data > Signed history**. Somebody else's is the **Check** tab, which is a different job on a different tab for exactly the reason this page exists: reading a history is how you decide whether to trust a stranger. ### Your own history Every published version, in order, with what changed at each one written out in words: a handle change, an account added or removed, an anchor moved, a recovery policy published or removed, and a key change with whether the handoff was authorized. **Check this history** runs steps 1 and 2 of the audit above on the phone. Every signature and every link is checked on the device, and the result says so plainly, "Checked on this phone", rather than reporting what a server concluded. Step 3 is not done for you, deliberately. The app does not fetch the `.ots` file and never asks a Bitcoin node anything, so a version reading as confirmed is RealHandles reporting on RealHandles, and the app words it that way. Check the proof file with any OpenTimestamps tool if you want the date to rest on something other than our word. ### Checking somebody else The **Check** tab takes a handle, fetches the proof, and verifies the signature and the whole chain on the device. It also does the pinning this page recommends, without being asked. The genesis key of every handle you check is remembered, listed on the Keys tab, and compared on every later check. If the key signing a handle now is not the one you trusted before, and nothing in the history authorizes the change, the app stops and says the name is being held by a stranger until that is explained. An authorized rotation passes through quietly, which is the property that lets you follow somebody through a key change without being told. --- # The signed manifest URL: https://docs.realhandles.com/concepts/the-signed-manifest/ Description: The JWS document that holds the identity, and the rules a server applies before storing it. The manifest is your identity. It is a compact JWS, signed by your key, whose payload lists your accounts, your disavowals, your anchor, your position in your own history, and the recovery policy in force. Our database is a cache of it. Everything we serve could be rebuilt from the signed file, and nothing we hold outranks it. ## What the file looks like ```jsonc { "$schema": "https://realhandles.com/schema/realhandles-v1.json", "jws": "eyJ...", // authoritative: compact JWS (EdDSA), payload = the manifest "manifest": { ... }, // human-readable mirror, never trusted on its own "publicKeyJwk": { ... }, // convenience copy of the signing public key "keyId": "base64url(SHA-256(pubkey))" } ``` Only `jws` counts. The `manifest` field is there so a person can read the file without decoding anything, and a verifier that finds the two disagreeing ignores the mirror and believes the signature. That is why verification re-parses the manifest from the bytes the signature covered, rather than from the copy sitting next to it. ## The question verification asks **Is this signed by the key I already trust?** Never "does a file exist at this domain", never "did the API return 200", never "is this person logged in". Those questions all have answers that somebody else can arrange. A signature has exactly one producer. The `keyId` is `base64url(SHA-256(...))` of the public key, canonicalized to `{crv, kty, x}` so it comes out the same on every platform. That fingerprint is what a verifier pins when it decides to trust you. ## The server never signs The server does exactly three things with a manifest: verify it, decide whether the key that signed it is allowed to, and store it. It holds no private key, so it has no way to produce a manifest on your behalf even if it wanted to. That is what makes "the key is the root of trust" a fact about the system rather than a promise about our conduct. ## What the server checks before storing 1. **The signature verifies**, and the `keyId` matches the embedded public key. 2. **The chain position is right.** Its `seq` and `prev` must continue from the head we actually hold. Get this wrong and you are told to reload and sign again, which is the normal outcome when two devices publish close together. Nothing is lost. 3. **The signing key is allowed.** Either it is the key already registered to you, or the manifest carries an authorized [rotation or recovery](/guides/recovery-and-rotation/). A login gets you nowhere here. It decides which identity row you may write to, and then the signature decides whether the write is legitimate. ## Why your edit is not public until you re-sign This is the single most confusing thing about the model, so here it is plainly. Adding an account, verifying a domain, writing a disavowal, or reordering a list changes **our copy** immediately. Your profile on `realhandles.com` shows it right away. Your signed proof does not contain it, because nothing has signed it yet. Until you publish and your key signs the new version: - Anyone verifying you independently sees the previous version, which is the correct answer to give. - The change exists only as our word for it, which is exactly the kind of claim this product exists to stop people from having to accept. - The dashboard says so, and the mobile app says so, because the alternative would be showing you a proof that is not one. The fix is not a background job. It is a signature, and only you can produce it. Publish. ![One manifest lists every published account and the anchor](../../../assets/signed-proof.png) One manifest lists every published account and the anchor. Re-signing is what moves an edit from the server into something anyone can check. ## What is inside the payload | Field | Purpose | | --- | --- | | `subject` | Handle, display name, public key, keyId | | `accounts` | Verified and claimed accounts, in the order you set | | `disavowed` | Signed "not me" statements | | `anchor` | One rotatable pointer to a hosted copy. Not the identity | | `seq`, `prev` | Position in your history and the hash of the version before it | | `rotation` | Present only on a version that changes the signing key | | `recovery` | Keys authorized in advance to take the identity back | | `issued`, `statement` | When, and the human-readable claim | `seq` and `prev` are optional for backward compatibility: a manifest without them is treated as the first version. Everything else about the format is additive by design, so an old verifier does not break when a new field appears. ## Verifying one yourself The reference verifier is published as [`@realhandles/verify`](https://www.npmjs.com/package/@realhandles/verify) and is the exact code this site runs, extracted verbatim. The spec, JSON Schema, and conformance vectors, including invalid ones that must fail, live in [realhandles/spec](https://github.com/realhandles/spec). ![The verify page on realhandles.com, which fetches a manifest and checks its signature](../../../assets/verify-page.png) The page above runs the same check in your browser. It fetches the manifest and tests the signature against the key in the file, so a wrong answer from this server would be visible rather than persuasive. Neither is required. It is a standard EdDSA JWS, so any JOSE library reaches the same conclusion, which is the entire point of choosing a boring format. ## In the Android app The app signs manifests. The rule that the server never signs is unchanged by that, and so is the rule that only your key can produce a version: the app has to hold the key and have it unlocked, and it says which of those is missing rather than offering a publish button that could only fail. The Identity tab carries the same split this page describes. A banner reads **Saved, but not published yet** while your changes are on realhandles.com and not yet in anything signed. **Publish & re-sign** is what closes it: your key signs the current state and a new entry joins your history. One thing the app does that the dashboard does not. If the version about to be signed would drop something the last one carried, it asks first and names what is going. Publishing cannot be undone, and the correct moment to notice a missing account is before the signature, not after. Two devices publishing close together produce the ordinary chain-position rejection described above. The app answers it the same way: reload and sign again. Nothing is lost.