Trust infrastructure for ATProto social applications
User trust scores (20-80 scale) that travel between apps. Good behavior on a dating app means you start with that same reputation on a professional network.
When someone gets reported on one app, it affects their reputation across the network. Prevents bad actors from platform-hopping.
Configurable systems that limit how many people users can contact per time period. Prevents spam, encourages intentional interactions.
Maps ATProto DIDs to apps, tracks verification status, enables cross-app queries without apps storing each other's user data.
Let's trace what happens when someone uses two apps powered by Glowrm:
Alice -> PBJ: Signs in with ATProto
PBJ -> Glowrm: "Register did:plc:alice123 in app 'pbj'"
Glowrm:
- Creates identity record
- Sets reputation: 50 (new recruit)
- Initializes SPA: 15 jars/week
- Returns: { reputation: 50, jars: 15 }
Alice -> PBJ: Sends 5 jars to Bob
PBJ -> Glowrm: "Use 5 units for did:plc:alice123"
Glowrm:
- Checks: Does Alice have 5 jars? Yes (15 available)
- Deducts 5 from her balance
- Records allocation
- Returns: { remaining: 10 }
PBJ -> Glowrm: "Report positive_interaction" Glowrm: - Creates trust event (type: positive) - Updates both reputations: +1 - Alice: 50 -> 51 - Bob: 50 -> 51
Alice -> PBJ: Reports Bob PBJ -> Glowrm: "Trust event - report, severity: high" Glowrm: - Creates trust event - Decreases Bob's reputation: 51 -> 48 - Flags for admin review
Admin -> Glowrm: Ban did:plc:bob456 (global) Glowrm: - Updates Bob's reputation: 48 -> 20 - Marks status: banned - Global flag: affects all apps
Bob -> Roster: Signs in with ATProto
Roster -> Glowrm: "Register did:plc:bob456 in app 'roster'"
Glowrm:
- Finds existing identity
- Checks reputation: 20 (banned)
- Returns: { reputation: 20, status: "banned" }
Roster -> Bob: "Cannot join - account flagged"
Alice -> Roster: Signs in
Roster -> Glowrm: "Register did:plc:alice123"
Glowrm:
- Finds existing identity
- Returns: { reputation: 51, status: "active" }
Roster: Shows Alice as trusted user
Why 20-80? Inspired by baseball scouting grades. 50 is average, scores cluster around the middle, outliers at the extremes are rare and meaningful.
| Score Range | Status | Description |
|---|---|---|
| 70-80 | Exceptional | Consistently positive interactions, helpful, responsive |
| 60-69 | Above Average | Good track record, few issues |
| 50-59 | Average | Normal range for new or moderately active users |
| 40-49 | Below Average | Some negative events, approaching problematic |
| 30-39 | Poor | Multiple reports, blocks, or suspicious patterns |
| 20-29 | Critical | Banned or serial bad actor |
Reputation is designed to be hard to game:
positive_interaction: Match, successful conversation, completed transactionreport: User reported for policy violationblock: User blocked by another userban: Administrative action removing userwarn: Warning issuedsuspend: Temporary suspension| Event Type | Impact |
|---|---|
| Positive interaction | +1 (max +15 from this source) |
| Report (low) | -1 |
| Report (medium) | -2 |
| Report (high) | -3 |
| Block | -3 |
| Ban | Sets to 20 |
The problem SPA solves: Unlimited actions lead to spam. Tinder-style swiping creates a "spray and pray" approach. LinkedIn InMail becomes a sales pitch fire hose.
The solution: Give users finite "units" (jars, credits, picks, waves) per time period. Forces strategic choices.
unitsTotal: How many per period (e.g., 15)unitName: What to call them (jars, credits, picks)periodDays: Refresh interval (7, 14, 30 days)allowVariableAmount: Can users send 1-10 or just 1?| App | Units | Name | Period | Variable? |
|---|---|---|---|---|
| PBJ (dating) | 15 | jars | 7 days | Yes (1-10) |
| Roster (professional) | 10 | credits | 30 days | Yes (1-5) |
| Roomies (friendship) | 20 | waves | 14 days | No (only 1) |
did:plc:abc123)| Data Type | Location | Who Controls |
|---|---|---|
| Profile (bio, photos) | User's ATProto repo | User |
| Conversations/messages | Individual app DB | App |
| Matches | Individual app DB | App |
| Reputation score | Glowrm | Glowrm |
| Trust events | Glowrm | Glowrm |
| Unit allocations | Glowrm | Glowrm |
| Verification status | Glowrm | Glowrm |
Why this separation?
curl -X POST https://glowrm.io/v1/apps/register \
-H "Content-Type: application/json" \
-d '{
"id": "my-app",
"name": "My App",
"displayName": "My App - Find Your People",
"appType": "dating",
"spaConfig": {
"unitsTotal": 15,
"allowVariableAmount": true,
"unitName": "credit",
"periodDays": 7
}
}'
Returns:
{
"appId": "my-app",
"apiKey": "glowrm_xxxxxxxxxxxxx"
}
GLOWRM_API_URL=https://glowrm.io GLOWRM_API_KEY=glowrm_xxxxxxxxxxxxx
// After user logs in with ATProto async function onUserLogin(atprotoSession) { const { did, handle } = atprotoSession; // Register with Glowrm const response = await fetch( `${GLOWRM_API_URL}/v1/identities/register`, { method: 'POST', headers: { 'Authorization': `Bearer ${GLOWRM_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ did, handle, appId: 'my-app' }) } ); const userData = await response.json(); // userData = { reputation: 50, spaState: {...} } return userData; }
async function getUserUnits(did) { const response = await fetch( `${GLOWRM_API_URL}/v1/spa/${did}/state?appId=my-app`, { headers: { 'Authorization': `Bearer ${GLOWRM_API_KEY}` } } ); const spaState = await response.json(); // { unitsRemaining: 12, unitsTotal: 15, periodEndsAt: "..." } return spaState; }
async function reportMatch(userDid, matchedDid) { await fetch(`${GLOWRM_API_URL}/v1/trust/events`, { method: 'POST', headers: { 'Authorization': `Bearer ${GLOWRM_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ subjectDid: matchedDid, actorDid: userDid, appId: 'my-app', type: 'positive_interaction', notes: 'Users matched' }) }); }
positive_interactionpositive_interactionreport with severityblock// Don't show exact scores to users // Instead, use ranges or indicators function reputationBadge(score) { if (score >= 70) return { text: "Trusted", color: "green" }; if (score >= 50) return { text: "Verified", color: "blue" }; if (score >= 30) return { text: "New", color: "gray" }; return { text: "Flagged", color: "red" }; }
15 jars per week, variable amounts (1-10 jars). Bilateral exchange unlocks messaging. Recipients see how many jars you spent.
10 credits per month, variable amounts (1-5 credits). 1 credit = quick question, 5 credits = mentorship request.
20 waves per 2 weeks, fixed amount (always 1 wave). Higher unit count because roommate search is often urgent.
Demo coming soon
No. Profile content (photos, bio, interests) lives in your ATProto repository. Apps read directly from your repo. Glowrm only tracks your DID, reputation score, and interaction history.
No. Your reputation is cross-app intentionally. The whole point is that behavior on one platform affects others. This creates accountability.
The reputation system is power-protected. One or two reports from new users barely affect your score. Multiple reports from trusted users matter more. Admins review high-severity events.
Apps can show you your recent trust events (matches, reports) but not detailed notes from other users (privacy). You know "reputation decreased due to report" but not exactly what was said.
Glowrm tracks per-app patterns. If one app suddenly reports 50% of users, that's flagged for admin review. Apps that abuse the system lose access.
Contact the app that banned you first. If it was a global ban, contact Glowrm directly. Provide context, explain what happened. Admins review and can adjust reputation or remove bans.
No. The whole system is designed to prevent fresh starts after bad behavior. If you've genuinely changed, build reputation back up over time through positive interactions.