TypeScript library for creating and publishing linked claims on ATProto (Bluesky)
A composable, type-safe library for working with verifiable claims on the AT Protocol. Implements the LinkedClaims specification from the Decentralized Identity Foundation (DIF).
- ✅ Fluent Builder API - Chainable, type-safe claim construction
- ✅ ATProto Native - Seamless integration with Bluesky/ATProto
- ✅ Claims-about-Claims - Built-in support for endorsements, disputes, revocations
- ✅ Content Hashing - Compute integrity hashes for evidence
- ✅ Schema Validation - Automatic validation against the
community.claimlexicon - ✅ Universal - Works in Node.js and browser environments
- ✅ TypeScript - Full type safety with excellent IDE support
npm install @cooperation/claim-atprotoRequirements:
- Node.js 18+ or modern browser
@atproto/api(peer dependency)
import { AtpAgent } from '@atproto/api'
import { ClaimClient, createClaim } from '@cooperation/claim-atproto'
// Authenticate with Bluesky
const agent = new AtpAgent({ service: 'https://bsky.social' })
await agent.login({
identifier: 'alice.bsky.social',
password: 'app-password', // Use an app password, not your main password
})
// Create a claim client
const client = new ClaimClient({ agent })
// Build and publish a claim
const claim = createClaim()
.subject('did:plc:alice')
.type('skill')
.object('React')
.statement('5 years of production experience')
.confidence(0.9)
.build()
const published = await client.publish(claim)
console.log(`Published at: ${published.uri}`)A claim is an immutable, signed assertion about any URI-addressable subject:
const claim = createClaim()
.subject('did:plc:alice') // Who/what the claim is about
.type('skill') // Category of claim
.object('TypeScript') // Optional: specific object
.statement('Expert level') // Human-readable explanation
.confidence(1.0) // Optional: confidence (0-1)
.build()Endorsements, disputes, and other meta-claims reference another claim's AT-URI:
import { createEndorsement } from '@cooperation/claim-atproto'
// Endorse another claim
const endorsement = createEndorsement(
'at://did:plc:alice/community.claim/xyz123',
'I can confirm Alice has these skills',
{ confidence: 1.0, howKnown: 'FIRST_HAND' }
).build()
await client.publish(endorsement)Add structured evidence with content hashing:
import { createSource, computeDigestMultibase } from '@cooperation/claim-atproto'
const evidenceHash = await computeDigestMultibase('Evidence content...')
const claim = createClaim()
.subject('https://ngo.org/project')
.type('impact')
.statement('Delivered 500 water filters')
.withSource(
createSource()
.uri('https://evidence.org/report.pdf')
.digest(evidenceHash)
.howKnown('WEB_DOCUMENT')
)
.build()createClaim()- Build a claim with fluent APIcreateSource()- Build evidence/provenance metadatacreateProof()- Build external proof (for future external signing support)
ClaimClient- Publish and manage claims on ATProto.publish(claim)- Publish to your repository.publishTo(did, claim)- Publish to another repository.get(uri)- Fetch a claim by AT-URI.delete(uri)- Delete a claim
createEndorsement(uri, statement, options)- Create an endorsementcreateDispute(uri, statement, options)- Create a disputecreateSuperseding(uri, statement)- Create an update/replacementcreateRevocation(uri, reason)- Create a revocationcomputeDigestMultibase(content)- Hash content for integrityfetchAndHash(uri)- Fetch and hash remote content
validateClaim(claim)- Validate against lexicon (throws on error)isValidClaim(claim)- Check validity (returns boolean)
const claim = createClaim()
.subject('did:plc:alice')
.type('skill')
.object('React')
.statement('3 years production experience')
.build()
const published = await client.publish(claim)const claim = createClaim()
.subject('https://example.org/ngo/project-123')
.type('impact')
.statement('Distributed 500 water filters in Kibera')
.withSource(
createSource()
.uri('ipfs://bafybei...')
.howKnown('FIRST_HAND')
.dateObserved(new Date('2024-12-15'))
)
.effectiveDate(new Date('2024-12-15'))
.build()
await client.publish(claim)import { createEndorsement } from '@cooperation/claim-atproto'
const endorsement = createEndorsement(
'at://did:plc:bob/community.claim/abc123',
'I worked with Bob for 2 years and can confirm his skills',
{ confidence: 1.0, howKnown: 'FIRST_HAND' }
).build()
await client.publish(endorsement)import { createDispute } from '@cooperation/claim-atproto'
const dispute = createDispute(
'at://did:plc:alice/community.claim/xyz789',
'The actual count was 200, not 500',
{
evidence: 'https://evidence.org/actual-count.pdf',
howKnown: 'WEB_DOCUMENT'
}
).build()
await client.publish(dispute)const rating = createClaim()
.subject('https://restaurant.example.com')
.type('rating')
.object('food-quality')
.stars(4)
.statement('Excellent pasta, slightly slow service')
.build()
await client.publish(rating)Full TypeScript support with exported types:
import type {
Claim,
ClaimSource,
EmbeddedProof,
PublishedClaim,
HowKnown,
ClaimClientConfig,
} from '@cooperation/claim-atproto'The claimType field is an open string. Common values include:
skill- Professional skillscredential- Certifications, degreesimpact- NGO/charity impact claimsendorsement- Endorsement of another claimdispute- Dispute of another claimrating- Star ratings (usestarsfield)review- Reviews with textmembership- Organization membershipsupersedes- Claim that replaces anotherrevocation- Claim revocation
You can use any string value that fits your use case.
All claims published to ATProto are automatically signed by the repository's signing key. This happens transparently when you call client.publish().
Who signed a claim?
- If published to user's own repo → signer is the user's DID
- If published to server's repo → signer is the server's DID
For external signing (MetaMask, DIDs, etc.), see the embeddedProof field in the types. External signing support may be added in future versions.
Claims are automatically validated against the community.claim lexicon before publishing:
import { validateClaim, isValidClaim } from '@cooperation/claim-atproto'
// Throws error if invalid
validateClaim(claim)
// Returns boolean
if (isValidClaim(claim)) {
await client.publish(claim)
}
// Disable validation for testing
const client = new ClaimClient({ agent, validate: false })The library works in browsers too:
<script type="module">
import { createClaim } from 'https://esm.sh/@cooperation/claim-atproto'
const claim = createClaim()
.subject('did:plc:alice')
.type('endorsement')
.build()
</script>Or with a bundler (Vite, Webpack, etc.):
import { ClaimClient, createClaim } from '@cooperation/claim-atproto'
// ... use normallySee the examples/ directory for complete working examples:
- basic-claim.ts - Simple skill claim
- endorsement.ts - Endorsing another claim
- with-evidence.ts - Claim with evidence and content hash
# Install dependencies
npm install
# Build
npm run build
# Test
npm test
# Type check
npm run type-check
# Run examples
npx tsx examples/node/basic-claim.ts- claim-lexicon - The
community.claimlexicon specification - @atproto/api - ATProto SDK
- LinkedClaims - DIF specification
MIT
Contributions welcome! Please open an issue or PR.
- Lexicon Issues: Report at the claim-lexicon repo
- Library Issues: Open an issue in this repo
- ATProto Questions: See ATProto docs