···
+
# Aggregator Setup Guide
+
This guide explains how to set up and register an aggregator with Coves instances.
+
- [Overview](#overview)
+
- [Architecture](#architecture)
+
- [Prerequisites](#prerequisites)
+
- [Quick Start](#quick-start)
+
- [Detailed Setup Steps](#detailed-setup-steps)
+
- [Authorization Process](#authorization-process)
+
- [Posting to Communities](#posting-to-communities)
+
- [Rate Limits](#rate-limits)
+
- [Security Best Practices](#security-best-practices)
+
- [Troubleshooting](#troubleshooting)
+
- [API Reference](#api-reference)
+
**Aggregators** are automated services that post content to Coves communities. They are similar to Bluesky's feed generators and labelers - self-managed external services that integrate with the platform.
+
**Key characteristics**:
+
- Self-owned: You create and manage your own PDS account
+
- Domain-verified: Prove ownership via `.well-known/atproto-did`
+
- Community-authorized: Moderators grant posting permission per-community
+
- Rate-limited: 10 posts per hour per community
+
- RSS feed aggregators (tech news, blog posts)
+
- Social media cross-posters (Twitter → Coves)
+
- Event notifications (GitHub releases, weather alerts)
+
- Content curation bots (daily links, summaries)
+
┌──────────────────────────────────────────────────────────┐
+
├──────────────────────────────────────────────────────────┤
+
│ Aggregator creates PDS account │
+
│ Proves domain ownership (.well-known) │
+
│ Registers with Coves (enters users table) │
+
│ Writes service declaration │
+
│ Jetstream indexes into aggregators table │
+
└──────────────────────────────────────────────────────────┘
+
┌──────────────────────────────────────────────────────────┐
+
│ 2. Per-Community Authorization │
+
├──────────────────────────────────────────────────────────┤
+
│ Moderator writes authorization record │
+
│ Jetstream indexes into aggregator_authorizations │
+
└──────────────────────────────────────────────────────────┘
+
┌──────────────────────────────────────────────────────────┐
+
│ 3. Posting (Ongoing) │
+
├──────────────────────────────────────────────────────────┤
+
│ Aggregator calls post creation endpoint │
+
│ - Author in users table ✓ │
+
│ - Author in aggregators table ✓ │
+
│ - Authorization exists ✓ │
+
│ - Rate limit not exceeded ✓ │
+
│ Post written to community's PDS │
+
│ Jetstream indexes post │
+
└──────────────────────────────────────────────────────────┘
+
**users** - All actors (users, communities, aggregators)
+
**aggregators** - Aggregator-specific metadata
+
CREATE TABLE aggregators (
+
display_name TEXT NOT NULL,
+
record_uri TEXT NOT NULL UNIQUE,
+
record_cid TEXT NOT NULL,
+
created_at TIMESTAMPTZ,
+
**aggregator_authorizations** - Community authorizations
+
CREATE TABLE aggregator_authorizations (
+
id BIGSERIAL PRIMARY KEY,
+
aggregator_did TEXT NOT NULL,
+
community_did TEXT NOT NULL,
+
enabled BOOLEAN NOT NULL DEFAULT true,
+
record_uri TEXT NOT NULL UNIQUE,
+
record_cid TEXT NOT NULL,
+
UNIQUE(aggregator_did, community_did)
+
1. **Domain ownership**: You must own a domain where you can host static files over HTTPS
+
2. **Web server**: Ability to serve the `.well-known/atproto-did` file
+
3. **Development tools**: `curl`, `jq`, basic shell scripting knowledge
+
4. **Email address**: For creating the PDS account
+
- Custom avatar image (PNG/JPEG/WebP, max 1MB)
+
- GitHub repository for source code transparency
+
We provide automated setup scripts:
+
cd scripts/aggregator-setup
+
# Make scripts executable
+
# Run setup scripts in order
+
./1-create-pds-account.sh
+
# (Upload .well-known to your web server)
+
./3-register-with-coves.sh
+
./4-create-service-declaration.sh
+
See [scripts/aggregator-setup/README.md](../../scripts/aggregator-setup/README.md) for detailed script documentation.
+
## Detailed Setup Steps
+
### Step 1: Create PDS Account
+
Your aggregator needs its own atProto identity (DID). The easiest way is to create an account on an existing PDS.
+
**Using an existing PDS (recommended)**:
+
curl -X POST https://bsky.social/xrpc/com.atproto.server.createAccount \
+
-H "Content-Type: application/json" \
+
"handle": "mynewsbot.bsky.social",
+
"email": "bot@example.com",
+
"password": "secure-password-here"
+
"refreshJwt": "eyJ...",
+
"handle": "mynewsbot.bsky.social",
+
"did": "did:plc:abc123...",
+
**Save these credentials securely!** You'll need the DID and access token for all subsequent operations.
+
**Alternative**: Run your own PDS or use `did:web` (advanced).
+
### Step 2: Prove Domain Ownership
+
To register with Coves, you must prove you own a domain by serving your DID at `https://yourdomain.com/.well-known/atproto-did`.
+
echo "did:plc:abc123..." > .well-known/atproto-did
+
**Upload to your web server** so it's accessible at:
+
https://rss-bot.example.com/.well-known/atproto-did
+
curl https://rss-bot.example.com/.well-known/atproto-did
+
# Should return: did:plc:abc123...
+
**Nginx configuration example**:
+
location /.well-known/atproto-did {
+
alias /var/www/.well-known/atproto-did;
+
default_type text/plain;
+
add_header Access-Control-Allow-Origin *;
+
### Step 3: Register with Coves
+
Call the registration endpoint to register your aggregator DID with the Coves instance.
+
**Endpoint**: `POST /xrpc/social.coves.aggregator.register`
+
curl -X POST https://api.coves.social/xrpc/social.coves.aggregator.register \
+
-H "Content-Type: application/json" \
+
"did": "did:plc:abc123...",
+
"domain": "rss-bot.example.com"
+
**Response** (Success):
+
"did": "did:plc:abc123...",
+
"handle": "mynewsbot.bsky.social",
+
"message": "Aggregator registered successfully. Next step: create a service declaration record at at://did:plc:abc123.../social.coves.aggregator.service/self"
+
1. Coves fetches `https://rss-bot.example.com/.well-known/atproto-did`
+
2. Verifies it contains your DID
+
3. Resolves your DID to get handle and PDS URL
+
4. Inserts you into the `users` table
+
**You're now registered!** But you need to create a service declaration next.
+
### Step 4: Create Service Declaration
+
Write a `social.coves.aggregator.service` record to your repository. This contains metadata about your aggregator and gets indexed by Coves' Jetstream consumer.
+
**Endpoint**: `POST https://your-pds.com/xrpc/com.atproto.repo.createRecord`
+
curl -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
+
-H "Content-Type: application/json" \
+
"repo": "did:plc:abc123...",
+
"collection": "social.coves.aggregator.service",
+
"$type": "social.coves.aggregator.service",
+
"did": "did:plc:abc123...",
+
"displayName": "RSS News Aggregator",
+
"description": "Aggregates tech news from various RSS feeds",
+
"sourceUrl": "https://github.com/yourname/rss-aggregator",
+
"maintainer": "did:plc:your-personal-did",
+
"createdAt": "2024-01-15T12:00:00Z"
+
"uri": "at://did:plc:abc123.../social.coves.aggregator.service/self",
+
- `avatar`: Blob reference to avatar image
+
- `configSchema`: JSON Schema for community-specific configuration
+
**Wait 5-10 seconds** for Jetstream to index your service declaration into the `aggregators` table.
+
## Authorization Process
+
Before you can post to a community, a moderator must authorize your aggregator.
+
### How Authorization Works
+
1. **Moderator decision**: Community moderator evaluates your aggregator
+
2. **Authorization record**: Moderator writes `social.coves.aggregator.authorization` to community's repo
+
3. **Jetstream indexing**: Record gets indexed into `aggregator_authorizations` table
+
4. **Posting enabled**: You can now post to that community
+
### Authorization Record Structure
+
**Location**: `at://{community_did}/social.coves.aggregator.authorization/{rkey}`
+
"$type": "social.coves.aggregator.authorization",
+
"aggregatorDid": "did:plc:abc123...",
+
"communityDid": "did:plc:community123...",
+
"createdBy": "did:plc:moderator...",
+
"createdAt": "2024-01-15T12:00:00Z",
+
"allowedCategories": ["tech", "news"]
+
### Checking Your Authorizations
+
**Endpoint**: `GET /xrpc/social.coves.aggregator.getAuthorizations`
+
curl "https://api.coves.social/xrpc/social.coves.aggregator.getAuthorizations?aggregatorDid=did:plc:abc123...&enabledOnly=true"
+
"aggregatorDid": "did:plc:abc123...",
+
"communityDid": "did:plc:community123...",
+
"communityHandle": "~tech@coves.social",
+
"createdAt": "2024-01-15T12:00:00Z",
+
## Posting to Communities
+
Once authorized, you can post to communities using the standard post creation endpoint.
+
**Endpoint**: `POST /xrpc/social.coves.community.post.create`
+
curl -X POST https://api.coves.social/xrpc/social.coves.community.post.create \
+
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
+
-H "Content-Type: application/json" \
+
"communityDid": "did:plc:community123...",
+
"text": "New blog post: Understanding atProto Identity\nhttps://example.com/post",
+
"createdAt": "2024-01-15T12:00:00Z",
+
"index": { "byteStart": 50, "byteEnd": 75 },
+
"$type": "social.coves.richtext.facet#link",
+
"uri": "https://example.com/post"
+
"uri": "at://did:plc:abc123.../social.coves.community.post/3k...",
+
1. **Authentication**: Valid JWT token
+
2. **Author exists**: DID in `users` table
+
3. **Is aggregator**: DID in `aggregators` table
+
4. **Authorization**: Active authorization for (aggregator, community)
+
5. **Rate limit**: Less than 10 posts/hour to this community
+
6. **Content**: Valid post structure per lexicon
+
**Per-community rate limit**: 10 posts per hour
+
This is tracked in the `aggregator_posts` table and enforced at the handler level.
+
**Why?**: Prevents spam while allowing useful bot activity.
+
- Batch similar content
+
- Post only high-quality content
+
- Respect community guidelines
+
- Monitor your posting rate
+
## Security Best Practices
+
### Credential Management
+
- Store credentials in environment variables or secret management
+
- Use HTTPS for all API calls
+
- Rotate access tokens regularly (use refresh tokens)
+
- Keep `aggregator-config.env` out of version control
+
- Hardcode credentials in source code
+
- Commit credentials to Git
+
- Share access tokens publicly
+
- Reuse personal credentials for bots
+
- Use HTTPS for `.well-known` endpoint
+
- Keep domain under your control
+
- Monitor for unauthorized changes
+
- Use DNSSEC if possible
+
- Use HTTP (will fail verification)
+
- Use shared/untrusted hosting
+
- Allow others to modify `.well-known` files
+
- Use expired SSL certificates
+
- Validate all external content before posting
+
- Sanitize URLs and text
+
- Rate-limit your own posting
+
- Implement circuit breakers for failures
+
- Post unvalidated user input
+
- Include malicious links
+
### Registration Errors
+
#### Error: "DomainVerificationFailed"
+
**Cause**: `.well-known/atproto-did` not accessible or contains wrong DID
+
1. Verify file is accessible: `curl https://yourdomain.com/.well-known/atproto-did`
+
2. Check content matches your DID exactly (no extra whitespace)
+
3. Ensure HTTPS is working (not HTTP)
+
4. Check web server logs for access errors
+
5. Verify firewall rules allow HTTPS traffic
+
#### Error: "AlreadyRegistered"
+
**Cause**: This DID is already registered with this Coves instance
+
- This is safe to ignore if you're re-running setup
+
- If you need to update info, just create a new service declaration
+
- Contact instance admin if you need to remove registration
+
#### Error: "DIDResolutionFailed"
+
**Cause**: Could not resolve DID document from PLC directory
+
1. Verify DID exists: `curl https://plc.directory/{your-did}`
+
2. Wait 30 seconds and retry (PLC propagation delay)
+
3. Check PDS is accessible
+
4. Verify DID format is correct (must start with `did:plc:` or `did:web:`)
+
#### Error: "NotAuthorized"
+
**Cause**: No active authorization for this (aggregator, community) pair
+
1. Check authorizations: `GET /xrpc/social.coves.aggregator.getAuthorizations`
+
2. Contact community moderator to request authorization
+
3. Verify authorization wasn't disabled
+
4. Wait for Jetstream to index authorization (5-10 seconds)
+
#### Error: "RateLimitExceeded"
+
**Cause**: Exceeded 10 posts/hour to this community
+
1. Wait for the rate limit window to reset
+
2. Batch posts to stay under limit
+
3. Distribute posts across multiple communities
+
4. Implement posting queue in your aggregator
+
### Service Declaration Not Appearing
+
**Symptoms**: Service declaration created but not in `aggregators` table
+
1. Wait 5-10 seconds for Jetstream to index
+
2. Check Jetstream consumer logs for errors
+
3. Verify record was created: Check PDS at `at://your-did/social.coves.aggregator.service/self`
+
4. Verify `$type` field is exactly `"social.coves.aggregator.service"`
+
5. Check `displayName` is not empty (required field)
+
### Registration Endpoint
+
**`POST /xrpc/social.coves.aggregator.register`**
+
did: string // DID of aggregator (did:plc or did:web)
+
domain: string // Domain serving .well-known/atproto-did
+
did: string // Registered DID
+
handle: string // Handle from DID document
+
message: string // Next steps message
+
- `InvalidDID`: DID format invalid
+
- `DomainVerificationFailed`: .well-known verification failed
+
- `AlreadyRegistered`: DID already registered
+
- `DIDResolutionFailed`: Could not resolve DID
+
**`GET /xrpc/social.coves.aggregator.getServices`**
+
Get aggregator service details.
+
- `dids`: Array of DIDs (comma-separated)
+
**`GET /xrpc/social.coves.aggregator.getAuthorizations`**
+
List communities that authorized an aggregator.
+
- `aggregatorDid`: Aggregator DID
+
- `enabledOnly`: Filter to enabled only (default: false)
+
**`GET /xrpc/social.coves.aggregator.listForCommunity`**
+
List aggregators authorized by a community.
+
- `communityDid`: Community DID
+
- `enabledOnly`: Filter to enabled only (default: false)
+
- [Aggregator PRD](PRD_AGGREGATORS.md) - Architecture and design decisions
+
- [atProto Guide](../../ATPROTO_GUIDE.md) - atProto fundamentals
+
- [Communities PRD](../PRD_COMMUNITIES.md) - Community system overview
+
- [Setup Scripts README](../../scripts/aggregator-setup/README.md) - Script documentation
+
For issues or questions:
+
1. Check this guide's troubleshooting section
+
2. Review the PRD and architecture docs
+
3. Check Coves GitHub issues
+
4. Ask in Coves developer community