Skip to main content

Scoring Algorithm

CivicPulse uses a contributor scoring system to recognise and reward civic participation. The algorithm is versioned β€” the current version is civicpulse-score-v1.


Algorithm: civicpulse-score-v1​

The score is computed as a weighted sum of all civic actions a user has taken. Actions are recorded in DynamoDB when they occur and the score is recalculated on each leaderboard fetch (or on-demand when a user views their own profile).

Weight Table​

ActionFieldPointsNotes
Filed an issue reportreportFiled50Per issue submitted
Reported issue resolvedreportResolved100Awarded when issue status changes to resolved
Upvote received on a reportupvoteReceived2Per upvote on any issue you reported
Added an image to an issueimageAdded15Per image uploaded via S3
Added a video to an issuevideoAdded25Per video uploaded via S3
Reported a high-priority issuehighPriority30Bonus awarded per high-priority report
Cast an upvote on another issuevoteCast5Per upvote you give to another user's report
Posted a message in a threadcommentMade3Per message posted to an issue thread
Joined a community impact eventeventJoined20Per event RSVP'd and attended
Weekly activity streakstreakBonus50Per complete week with at least one civic action
Cross-category activitycategoryBonus10Bonus per unique category you contribute to (max 6)

Score Formula​

score =
(reportFiled Γ— 50) +
(reportResolved Γ— 100) +
(upvoteReceived Γ— 2) +
(imageAdded Γ— 15) +
(videoAdded Γ— 25) +
(highPriority Γ— 30) +
(voteCast Γ— 5) +
(commentMade Γ— 3) +
(eventJoined Γ— 20) +
(streakBonus Γ— 50) +
(categoryBonus Γ— 10)

Tiers​

Tiers are assigned based on total score thresholds:

TierScore RangeDescription
Citizen0 – 99Starting tier β€” every registered user
Contributor100 – 299Active civic participant
Champion300 – 599Regular, high-quality contributor
Legend600+Elite civic contributor

Tier is displayed on the user's profile, in the leaderboard, and beside their name in issue threads.


Badges​

Badges are awarded for specific milestones. They are independent of tiers β€” a user can earn multiple badges at any tier.

BadgeCriteriaDescription
PioneerAmong first 100 reporters on the platformEarly adopter recognition
Community VoiceReceived 100+ total upvotes on reported issuesYour reports resonate with the community
Problem Solver5+ reported issues reached resolved statusYou report issues that get fixed
Evidence ExpertAdded 10+ images or videos across issuesDocumenting issues with media proof
Civic ChampionReached the Champion tierScored 300+ points
LegendReached the Legend tierScored 600+ points
All-RounderContributed to all 6 categoriesBroad civic engagement across areas
Civic ActivistCompleted 4+ consecutive weekly activity streaksConsistent long-term participation

Badges are stored as an array of badge IDs on the user's score record and include the earnedAt timestamp.


Score Computation​

Scores are not stored as a running total β€” they are recomputed from the raw activity log each time to ensure accuracy if weights change or actions are retrospectively credited or revoked.

The computation reads all SCORE_ACTIVITY records for a user from DynamoDB and applies the weight table:

function computeScore(activities) {
const counts = {
reportFiled: 0, reportResolved: 0, upvoteReceived: 0,
imageAdded: 0, videoAdded: 0, highPriority: 0,
voteCast: 0, commentMade: 0, eventJoined: 0,
streakBonus: 0, categoryBonus: 0,
};

for (const activity of activities) {
if (counts[activity.type] !== undefined) {
counts[activity.type]++;
}
}

// Category bonus: count distinct categories, cap at 6
const uniqueCategories = new Set(
activities
.filter(a => a.type === 'reportFiled' && a.category)
.map(a => a.category)
).size;
counts.categoryBonus = Math.min(uniqueCategories, 6);

const weights = {
reportFiled: 50, reportResolved: 100, upvoteReceived: 2,
imageAdded: 15, videoAdded: 25, highPriority: 30,
voteCast: 5, commentMade: 3, eventJoined: 20,
streakBonus: 50, categoryBonus: 10,
};

return Object.entries(counts).reduce((total, [key, count]) => {
return total + count * (weights[key] || 0);
}, 0);
}

Sharing Your Score​

Each contributor has a public shareable URL:

https://civicpulse.in/contributors/<userId>

This page renders the user's score card with their tier, badges, and a contribution breakdown. The page is publicly accessible (no login required) and is designed to be shared on social media.

The API returns this URL in the shareUrl field of GET /api/scores/:userId.


API Payload Structure​

The full score payload returned by the API:

{
"userId": "usr_01HN...",
"displayName": "Priya S.",
"city": "bangalore",
"score": 1240,
"tier": "Legend",
"rank": 1,
"globalRank": 7,
"badges": [
{
"id": "Pioneer",
"displayName": "Pioneer",
"description": "One of the first 100 reporters on the platform",
"earnedAt": "2025-01-01T00:00:00.000Z"
}
],
"breakdown": {
"reportsField": 8,
"reportsResolved": 4,
"upvotesReceived": 210,
"imagesAdded": 12,
"videosAdded": 3,
"highPriorityBonus": 5,
"votesCast": 89,
"commentsMade": 44,
"eventsJoined": 6,
"streakBonus": 2,
"categoryBonus": 4
},
"algorithm": "civicpulse-score-v1",
"shareUrl": "https://civicpulse.in/contributors/usr_01HN...",
"computedAt": "2025-01-15T10:30:00.000Z"
}

Algorithm Versioning​

If the weight table changes in a future release, the algorithm version will be incremented to civicpulse-score-v2. The old algorithm version is stored alongside each score record, making it possible to audit or recompute historical scores under any version.

The current algorithm version is always reflected in the algorithm field of all score API responses.