@fujocoded/atproto-badges
v0.2.0
Published
ATProto badge attestation utilities
Downloads
27
Readme
@fujocoded/atproto-badges
Signed badges on ATproto. Officially Certify™ whatever your heart desires: events, communities, inside-jokes, friends, and anything in-between!
What is @fujocoded/atproto-badges?
@fujocoded/atproto-badges lets you create and sign badges on ATProto. You
define a badge (like "Yuletide 2026 Writer" or "ATmosphereConf 2026 attendee"),
sign it with your secret key, and write it to the recipient's PDS, where anyone can
verify it came from you.
Under the hood, it handles the cryptographic attestation (DAG-CBOR hashing, P-256 signing, PLC document updates) so you can focus on when and why to award badges, not how the signatures work.
What's included in @fujocoded/atproto-badges?
In this package, you'll find utilities to manage:
- Key management
generateSigningKeyscreates a new key pair for signing badgesloadSigningKeyloads a previously saved key so you can sign with it again
- Badge definitions
createBadgeDefinitioncreates a new badge type on your PDSfindExistingBadgeDefinitionchecks if a badge type already exists, so you don't create duplicates
- Badge awards
createBadgeAwardRecordbuilds a signed badge award, ready to write to a recipient's PDSgetExistingBadgeAwardchecks if someone already has a particular badgegetBadgeRkeygives you a deterministic record key, so concurrent requests don't create duplicate awards
- PLC updates
addAttestationVerificationMethodpublishes your public key to your DID document, so others can verify your signatures
- Verification
verifyBadgeAwardchecks whether a badge award's signature is legit — looks up the issuer's DID document and verifies the cryptographic signature
- Lower-level signing (if you're building something custom)
createRecordSignaturesigns any ATProto record, not just badgesgetRecordHashcomputes the hash that gets signed — useful for verification or multi-signer workflows
What can you do with @fujocoded/atproto-badges?
- Award participation badges for events & exchanges: give artists, writers, and other participants a verifiable badge that lives in their ATProto account— whether you're hosting Yuletide or a smaller shipping week
- Recognize contributors: zine participants, community moderators, event volunteers, code contributors...whatever you want to celebrate, put a badge on it!
- Verify badges: — use
verifyBadgeAwardto confirm a badge is legit by checking the issuer's signature against their published key - Build tools to mint and manage badges: these bad ~~boy~~badges can fit so many use cases within them!
Installation
npm add @fujocoded/atproto-badgesGetting started
Here's the typical flow, from setup to awarding your first badge.
At high level:
- Generate your secret key to sign badges with and store them safely!
- Publish your public key on your Identity Document™
- Create a badge definition in your PDS...
- ...then award it someone, with a signed copy in their PDS!
1. Generate your signing key
This generates your super ultra mega secret credentials that allow you to sign badges, pinkie-promising it is indeed you. You only need to do this once!
import fs from "node:fs";
import { generateSigningKeys } from "@fujocoded/atproto-badges";
const keys = await generateSigningKeys();
// If you want, you can save them to files
// This will be BADGE_PRIVATE_KEY in your secrets
fs.writeFileSync("./private.key", keys.privateKeyBase64url);
// This is your public key, for step 2
fs.writeFileSync("./public.key.txt", keys.publicDidKey);[!IMPORTANT]
You must keep the private key somewhere safe and never show it to anyone. If you lose it, you won't be able to sign with that key anymore; if someone steals it, they'll be able to sign as you.
When using private keys in your programs, make sure to use environment variables rather than hardcoding them. Never commit them to Git.
2. Publish your public key
To let everyone know you're the one whose secret key has been going around signing badges left and right, you must first upload the corresponding public key to your DID document—that is, to your ATproto "id card".
This command will send a verification email, and update your PLC document with the content of the key:
import { addAttestationVerificationMethod } from "@fujocoded/atproto-badges";
// Your DID, in this case the "yuletide exchange"
const exchangeDid = "did:plc:yuletide";
// First, trigger the verification email:
await agent.com.atproto.identity.requestPlcOperationSignature();
// Then, once you have the token from the email:
await addAttestationVerificationMethod({
// Check out @fujocoded/authproto if you have an
// Astro site!
agent,
// Your identity
did: exchangeDid,
// Your public key
publicDidKey: keys.publicDidKey,
token,
});3. Create a badge definition
A badge definition describes what the badge is. You create it once, put it on your PDS, then reference it every time you award it:
import {
createBadgeDefinition,
findExistingBadgeDefinition,
} from "@fujocoded/atproto-badges";
// Check if it already exists first!
const existing = await findExistingBadgeDefinition({
agent,
did: exchangeDid,
name: "Yuletide 2026 Writer",
});
if (existing) {
return "don't be greedy!";
}
const badgeDefinition = await createBadgeDefinition({
agent,
// Badge owner
did: exchangeDid,
// Badge name
name: "Yuletide 2026 Writer",
// Badge description
description: "Completed a gift fic for Yuletide 2026",
});[!NOTE]
The
agentused forputRecordmust be authenticated as the issuer of the badge. This establishes the legitimacy of the badge.
4. Award the badge
import {
createBadgeAwardRecord,
getExistingBadgeAward,
getBadgeRkey,
loadSigningKey,
} from "@fujocoded/atproto-badges";
const participantDid = "did:plc:participant";
// Don't award it twice!
const currentAward = await getExistingBadgeAward({
agent,
did: participantDid,
badgeDefinitionUri: badgeRef.uri,
});
if (currentAward) {
return "don't be greedy!";
}
// Get your key ready to sign!
const signingKey = await loadSigningKey({
privateKeyBase64url: process.env.BADGE_PRIVATE_KEY!,
});
// "I hereby award you the badge—"
const award = await createBadgeAwardRecord({
// The badge recipient
recipientDid: participantDid,
// Reference returned by createBadgeDefinition
badgeRef: badgeDefinition,
// Your DID
organizerDid: exchangeDid,
signingKey,
});
// Save the badge to the recipients PDS
await agent.com.atproto.repo.putRecord({
repo: participantDid,
collection: "community.lexicon.badge.award",
rkey: getBadgeRkey({ badgeDefinitionUri: badgeRef.uri }),
record: award,
});[!Note]
The
agentused forputRecordmust be authenticated as the recipient of the badge, not the issuer.The recipient claims their badge by writing the issuer-signed badge to their own PDS. The issuer never needs write access to the recipient's repo—the signature itself proves legitimacy.
Good to know
- Badge definitions live in the issuing organization's repo. Badge awards go in the recipient's repo.
getExistingBadgeAwardlooks up a badge award by definition URI and returns the full record value. You can check the CID yourself if you need to distinguish between versions of a badge definition.getBadgeRkeyderives the record key from the badge definition URI. This means awarding the same badge definition to the same person always targets the same record (easier to avoid duplicates!).- This package handles signing and data — you bring your own
AtpAgent, authentication, and app logic around it.
[!WARNING]
All parameters to
createBadgeAwardRecordmust be defined — passingundefinedfor any field (e.g. an unset env var fororganizerDid) will throw with a message likeCannot CBOR-encode record: field "organizerDid" is undefined. ATProto records are CBOR-encoded, and CBOR has no concept ofundefined.
Based on
The attestation signing in this package is based on the
atproto-attestation
Rust crate by smokesignal.events. If you're looking for a full Rust
implementation (including CLI tools for signing and verifying attestations),
check that out!
Support Us
You can check out more of our plugins here:
You can also become a patron or buy some merch:
