Creating a did-method-web Identity for ATProtocol
Published by @smokesignal.events on 2025-08-18 14:00 UTC.
If you're in the ATmosphere and creating posts, chances are that you have a did-method-plc identity. Onboarding into https://bsky.app/ uses that DID method by default, and it is the most user-friendly way to get started. It isn't the only option, however.
ATProtocol supports both did-method-plc, as well as did-method-web. A did-method-web identity isn't that different in terms of the type of content stored in the DID document. It has the ID field, list of also known as aliases, verification methods, and services. The main difference is the way they are resolved.
Why Consider did-method-web?
Now, there are some really great reasons to use did-method-plc over did-method-web. The first is that it's hands-off. You don't need to host your DID document and your practical surface area for security risk is really low. However, there are a few good reasons to have your own did-method-web identity.
First, you control your DID document. If the plc.directory domain or future PLC resolution system fundamentally changes or goes away, yours simply won't. Granted, you may have bigger problems to deal with at that point, but the control remains in your hands.
Second, you want to obscure your DID document history. There are practical reasons why vulnerable groups may not want to broadcast going from the handle nick-is-a-great-guy.com
to nicole-is-nice.com
. Privacy and safety considerations can make this control invaluable.
A note on handles: having a did-method-web identity doesn't mean you can't change handles. It only affects where your DID document is retrieved from. The flexibility of handle management remains intact while you maintain control over the underlying identity infrastructure.
Getting Started
This walk-through uses the ngerakines/atproto-tools
container, so it assumes you have docker or another way to run containers locally. For the sake of this walk-through, we're going to use the live production domain whatthecommit.com.
Setting Up the DID Document
First, we'll want to host a DID document with a verification method so that it is accessible to be retrieved. The id
field will have the value did:web:
followed by the hostname whatthecommit.com
. An empty DID document looks like this:
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/suites/secp256k1-2019/v1"
],
"alsoKnownAs": [
"at://whatthecommit.com"
],
"id": "did:web:whatthecommit.com",
"service": [],
"verificationMethod": []
}
On its own, that's not super helpful. We'll need to populate it with a service and verification method to make it functional.
Generating Your Signing Key
For the service, this should reflect and point to the PDS that you want the account to reside in. For the verification method, run the atproto-identity-key
command to generate your initial signing key:
$ docker run atproto-tools atproto-identity-key generate p256
p256 private: did:key:z42tXXXXXXXXXX
p256 public: did:key:zDnaendgESfFX8cCUtersmjGFEAMdVN6S1Nq2RhUgbFYk7J1d
This will output a private and public key pair. Take the key data portion of the public key (it starts with zDn
) and add it to the DID document:
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/suites/secp256k1-2019/v1"
],
"alsoKnownAs": [
"at://whatthecommit.com"
],
"id": "did:web:whatthecommit.com",
"service": [
{
"id": "#atproto_pds",
"serviceEndpoint": "https://pds.cauda.cloud",
"type": "AtprotoPersonalDataServer"
}
],
"verificationMethod": [
{
"controller": "did:web:whatthecommit.com",
"id": "did:web:whatthecommit.com#atproto",
"publicKeyMultibase": "zDnaendgESfFX8cCUtersmjGFEAMdVN6S1Nq2RhUgbFYk7J1d",
"type": "Multikey"
}
]
}
Save this so it can be retrieved via a GET request to /.well-known/did.json
with the content type "application/json". For example, with the above hostname, the full URL would be https://whatthecommit.com/.well-known/did.json
. It is really important to reference the correct identifiers for the service and verification method.
Configuring Handle Resolution
With the DID document available, we're going to now create another file to support ATProtocol handle resolution. Although it may seem redundant, the handle that you use and reference won't work automatically just because you're using a did-method-web identity.
Make a file available with the content "did:web:whatthecommit.com" (without quotes, spaces, or new lines) so it can be retrieved via a GET request to /.well-known/atproto-did
with the content type "text/plain". For example, with the above hostname, the full URL would be https://whatthecommit.com/.well-known/atproto-did
.
Preparing the PDS
Next we're going to prepare the PDS to accept the identity. That involves creating an invitation (if the PDS is invite-only) and creating an account in the PDS for the identity.
Creating an Invitation Code
If the PDS that the identity is going to does not require invitations, you can skip this part. Otherwise, using curl
, create an invitation code using your admin password:
$ curl --fail --silent --show-error --request POST --user "admin:PASSWORD" --header "Content-Type: application/json" --data '{"useCount": 1}' "https://pds.cauda.cloud/xrpc/com.atproto.server.createInviteCode"
{"code":"pds-cauda-cloud-abcde-fghij"}
Creating the Account
To create the account, we'll need a few things. First, an inter-service access token that is signed using the private key associated with the public key in our DID document. Second, the email address that we want to associate the account with. Third, the password that we want to use. And optionally, the invite code from the previous step.
The request body will look like this:
{
"email": "my_great_email..",
"handle": "whatthecommit.com",
"password": "password",
"did": "did:web:whatthecommit.com",
"inviteCode": "pds-cauda-cloud-abcde-fghij"
}
To create the inter-service JWT, we're going to reference the private key from above and set some required parameters. Run the atproto-oauth-service-token
command to generate the token:
$ docker run atproto-tools atproto-oauth-service-token did:web:whatthecommit.com did:key:z42tXXXXXXXXXX did:web:pds.cauda.cloud lxm=com.atproto.server.createAccount
eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDprZXk6ekRuYWVuZGdFU2ZGWDhjQ1V0ZXJzbWpHRkVBTWRWTjZTMU5xMlJoVWdiRllrN0oxZCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6d2ViOndoYXR0aGVjb21taXQuY29tIiwiYXVkIjoiZGlkOndlYjpwZHMuY2F1ZGEuY2xvdWQiLCJleHAiOjE3NTU1Mjg5NzgsImlhdCI6MTc1NTUyODkxOCwianRpIjoiMDFrMnl0dHgyMTZ2d2tjYnA0eTB3MzdhMmIiLCJseG0iOiJjb20uYXRwcm90by5zZXJ2ZXIuY3JlYXRlQWNjb3VudCJ9.HuDp0UA1RwsTCaMJ8Il-Ss_MRsnG5f0-gMvNPuYXVdhtSdVYSU3XAZJylFY8hMT8NeimyjR6oWbAAIaA0G5Rcg
That command is going to produce a token that is good for 60 seconds, so all together the request will look like this:
$ curl --fail --silent --show-error --request POST --header "Authorization: Bearer eyJhbGciOi...AIaA0G5Rcg" --header "Content-Type: application/json" --data '{ "email": "my_great_email..", "handle": "whatthecommit.com", "password": "password", "did": "did:web:whatthecommit.com", "inviteCode": "pds-cauda-cloud-abcde-fghij"}' "https://pds.bowfin-woodpecker.ts.net/xrpc/com.atproto.server.createAccount"
{
"accessJwt": "eyJ0eXAiOi...O09p1jobpA",
"refreshJwt": "...",
"handle": "whatthecommit.com",
"did": "did:web:whatthecommit.com",
"didDoc": { }
}
Finalizing the DID Document
The response contains an access JWT that we need to use to update the DID document one last time. We'll fetch the recommended DID credentials from the PDS:
$ curl --fail --silent --show-error --request GET --header "Authorization: Bearer eyJ0eXAiOi...O09p1jobpA" https://pds.cauda.cloud/xrpc/com.atproto.identity.getRecommendedDidCredentials
{"alsoKnownAs":["at://whatthecommit.com"],"verificationMethods":{"atproto":"did:key:zQ3shWLE1FnPDA6jc6EncD5bzRATVv2pqLm3wo7y5pVyuSgET"},"rotationKeys":["did:key:zQ3shSgMaSd8PmZDy63CvjixESX8Ef6nKumUg9gWUry764SC8"],"services":{"atproto_pds":{"type":"AtprotoPersonalDataServer","endpoint":"https://pds.cauda.cloud"}}}
The rotation key isn't really necessary because we aren't performing any rotation operations. We do, however, want to take the provided "atproto" verification method and use it in our DID document. In my case, I want to rename the existing verification method to "signing" and use the provided value as the actual "atproto" identified key.
My updated DID document looks like this now:
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/suites/secp256k1-2019/v1"
],
"alsoKnownAs": [
"at://whatthecommit.com"
],
"id": "did:web:whatthecommit.com",
"service": [
{
"id": "#atproto_pds",
"serviceEndpoint": "https://pds.cauda.cloud",
"type": "AtprotoPersonalDataServer"
}
],
"verificationMethod": [
{
"controller": "did:web:whatthecommit.com",
"id": "did:web:whatthecommit.com#atproto",
"publicKeyMultibase": "zQ3shWLE1FnPDA6jc6EncD5bzRATVv2pqLm3wo7y5pVyuSgET",
"type": "Multikey"
},
{
"controller": "did:web:whatthecommit.com",
"id": "did:web:whatthecommit.com#signing",
"publicKeyMultibase": "zDnaendgESfFX8cCUtersmjGFEAMdVN6S1Nq2RhUgbFYk7J1d",
"type": "Multikey"
}
]
}
Activating the Account
Lastly, activate the account with curl
:
$ curl --fail --silent --show-error --request POST --header "Authorization: Bearer eyJ0eXAiOi...O09p1jobpA" https://pds.cauda.cloud/xrpc/com.atproto.server.activateAccount
Wrapping Up
And there you have it. You've successfully created a did-method-web identity for ATProtocol. This approach gives you direct control over your DID document while still maintaining full compatibility with the ATProtocol ecosystem. The process involves several steps and requires careful attention to detail, particularly around key management and proper hosting of the well-known files, but the result is a self-sovereign identity that you fully control.
Remember that with this control comes responsibility. You'll need to ensure your DID document remains accessible and properly configured. Consider implementing proper backup strategies for your keys and monitoring for your hosting infrastructure. The trade-off between convenience and control is ultimately yours to make, but for those who value sovereignty over their digital identity, did-method-web provides a compelling option in the ATProtocol ecosystem.