Overview
This guide explains how to create custom achievements tailored to your application’s needs. Custom achievements enable you to reward users for specific behaviors, milestones, or accomplishments unique to your platform.
Custom achievements should align with your platform’s goals and provide meaningful recognition for user accomplishments.
Achievement Design
Planning Your Achievement
Before creating an achievement, consider:
- Purpose: What behavior are you rewarding?
- Requirements: What must users do to earn it?
- Rarity: How difficult should it be?
- Rewards: What points/boost should it grant?
- Verification: How will you verify completion?
Achievement Categories
Choose the most appropriate category:
- DeFi: Financial protocol interactions
- NFT: NFT-related activities
- Social: Community engagement
- Trading: Trading performance
- Governance: DAO participation
- Gaming: Gaming achievements
- Identity: Identity verification
- Trust: Trust-building activities
Rarity Guidelines
| Rarity | Difficulty | User % | Points | Boost |
|---|
| Common | Easy | 60% | 10-50 | +1% |
| Uncommon | Moderate | 30% | 50-100 | +2% |
| Rare | Challenging | 8% | 100-250 | +5% |
| Epic | Very Hard | 1.5% | 250-500 | +10% |
| Legendary | Extremely Rare | 0.5% | 500-1000 | +20% |
Requirement Types
1. Score Threshold
Require users to reach a minimum score.
// Encode requirement data
const requirementData = ethers.utils.defaultAbiCoder.encode(
['uint256'], // minimum score
[500]
);
// Create achievement
await contract.createAchievement(
'Rising Star',
'Reach a ZKScore of 500',
'ipfs://QmXyz.../rising-star.png',
6, // Identity category
1, // Uncommon rarity
75, // 75 points
150, // 1.5% boost
1, // SCORE_THRESHOLD type
requirementData
);
2. Category Score
Require minimum score in a specific category.
// Encode requirement: category index and minimum score
const requirementData = ethers.utils.defaultAbiCoder.encode(
['uint8', 'uint256'],
[0, 300] // DeFi category, 300 minimum
);
await contract.createAchievement(
'DeFi Expert',
'Reach 300 DeFi score',
'ipfs://QmXyz.../defi-expert.png',
0, // DeFi category
2, // Rare
150,
300,
2, // CATEGORY_SCORE type
requirementData
);
3. Activity Count
Require a certain number of activities.
// Encode requirement: activity type and count
const requirementData = ethers.utils.defaultAbiCoder.encode(
['bytes32', 'uint256'],
[
ethers.utils.id('NFT_TRADE'), // activity type hash
50 // required count
]
);
await contract.createAchievement(
'Active Trader',
'Complete 50 NFT trades',
'ipfs://QmXyz.../active-trader.png',
1, // NFT category
1, // Uncommon
100,
200,
3, // ACTIVITY_COUNT type
requirementData
);
4. Time-Based
Require users to be active for a certain duration.
// Encode requirement: duration in seconds
const requirementData = ethers.utils.defaultAbiCoder.encode(
['uint256'],
[180 * 24 * 60 * 60] // 180 days
);
await contract.createAchievement(
'Veteran Member',
'Be active for 6 months',
'ipfs://QmXyz.../veteran.png',
6, // Identity category
2, // Rare
200,
400,
4, // TIME_BASED type
requirementData
);
5. Composite
Combine multiple requirements.
// Encode multiple requirements
const requirementData = ethers.utils.defaultAbiCoder.encode(
['uint8[]', 'uint256[]'],
[
[0, 1, 2], // Categories: DeFi, NFT, Social
[200, 200, 200] // Minimum scores for each
]
);
await contract.createAchievement(
'Well-Rounded',
'Score 200+ in DeFi, NFT, and Social',
'ipfs://QmXyz.../well-rounded.png',
6, // Identity category
3, // Epic
400,
800,
5, // COMPOSITE type
requirementData
);
6. Proof-Based
Require zero-knowledge proof verification.
// Encode proof verification parameters
const requirementData = ethers.utils.defaultAbiCoder.encode(
['address', 'bytes32'],
[
'0xVerifierContractAddress',
ethers.utils.id('PROTOCOL_PARTICIPATION')
]
);
await contract.createAchievement(
'Privacy Champion',
'Verify protocol participation with ZK proof',
'ipfs://QmXyz.../privacy-champion.png',
7, // Trust category
4, // Legendary
1000,
2000,
6, // PROOF_BASED type
requirementData
);
Complete Creation Example
DeFi Protocol Achievement
const { ethers } = require('ethers');
async function createDeFiAchievement() {
const provider = new ethers.providers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_KEY');
const signer = provider.getSigner();
const contract = new ethers.Contract(
'0x3456789012345678901234567890123456789012',
ACHIEVEMENT_REGISTRY_ABI,
signer
);
// Achievement parameters
const name = 'Liquidity Provider Pro';
const description = 'Provide liquidity across 5 different protocols';
const imageURI = 'ipfs://QmXyz.../lp-pro.png';
const category = 0; // DeFi
const rarity = 2; // Rare
const points = 200;
const scoreBoost = 400; // 4%
// Encode requirement: 5 different protocols
const requirementData = ethers.utils.defaultAbiCoder.encode(
['bytes32', 'uint256'],
[
ethers.utils.id('LIQUIDITY_PROVIDED'),
5 // 5 protocols
]
);
const requirementType = 3; // ACTIVITY_COUNT
// Create achievement
const tx = await contract.createAchievement(
name,
description,
imageURI,
category,
rarity,
points,
scoreBoost,
requirementType,
requirementData
);
const receipt = await tx.wait();
// Get achievement ID from event
const event = receipt.events.find(e => e.event === 'AchievementCreated');
const achievementId = event.args.achievementId;
console.log(`Achievement created!`);
console.log(`ID: ${achievementId.toString()}`);
console.log(`Name: ${name}`);
console.log(`Category: DeFi`);
console.log(`Rarity: Rare`);
console.log(`Points: ${points}`);
console.log(`Boost: ${scoreBoost / 100}%`);
return achievementId;
}
Image Guidelines
- Format: PNG with transparency
- Size: 512x512 pixels minimum
- Style: Consistent with your brand
- Quality: High resolution for badges
- Storage: IPFS or decentralized storage
Creating Badge Images
// Upload to IPFS
const FormData = require('form-data');
const fs = require('fs');
const axios = require('axios');
async function uploadBadgeToIPFS(imagePath) {
const formData = new FormData();
formData.append('file', fs.createReadStream(imagePath));
const response = await axios.post(
'https://api.pinata.cloud/pinning/pinFileToIPFS',
formData,
{
headers: {
'Authorization': `Bearer ${process.env.PINATA_JWT}`,
...formData.getHeaders()
}
}
);
const ipfsHash = response.data.IpfsHash;
const imageURI = `ipfs://${ipfsHash}`;
console.log(`Badge uploaded: ${imageURI}`);
return imageURI;
}
Description Guidelines
- Length: 50-150 characters
- Clarity: Clear requirements
- Tone: Encouraging and positive
- Action: Describe what to do
- Benefit: Mention rewards
Verification Setup
Manual Verification
For achievements requiring manual review:
class ManualVerifier {
constructor(contract, account) {
this.contract = contract;
this.account = account;
}
async verifyUser(userAddress, achievementId, proofData) {
// Verify proof data
const isValid = await this.validateProof(proofData);
if (!isValid) {
throw new Error('Invalid proof data');
}
// Update progress to 100%
const achievement = await this.contract.getAchievement(achievementId);
const requiredProgress = achievement.requirementData; // depends on type
await this.contract.updateProgress(
userAddress,
achievementId,
requiredProgress
);
console.log(`User ${userAddress} verified for achievement ${achievementId}`);
}
async validateProof(proofData) {
// Implement your validation logic
return true;
}
}
Automatic Verification
For automated achievements:
class AutomaticVerifier {
constructor(contract) {
this.contract = contract;
}
// Listen for relevant events
async monitorActivity(userAddress, activityType) {
// Monitor blockchain events
const filter = {
address: PROTOCOL_ADDRESS,
topics: [
ethers.utils.id(activityType),
ethers.utils.hexZeroPad(userAddress, 32)
]
};
provider.on(filter, async (log) => {
await this.incrementProgress(userAddress, log);
});
}
async incrementProgress(userAddress, log) {
// Parse event data
const eventData = parseEventLog(log);
// Update achievement progress
const achievementIds = await this.getRelatedAchievements(eventData.type);
for (const id of achievementIds) {
const currentProgress = await this.contract.getProgress(userAddress, id);
const newProgress = currentProgress.current + 1;
await this.contract.updateProgress(userAddress, id, newProgress);
}
}
async getRelatedAchievements(activityType) {
// Return achievement IDs that track this activity
return [123, 456, 789];
}
}
Testing Custom Achievements
Testing on Testnet
async function testAchievement(achievementId) {
// 1. Create test user
const testUser = ethers.Wallet.createRandom().connect(provider);
// 2. Fund test user
await signer.sendTransaction({
to: testUser.address,
value: ethers.utils.parseEther('0.1')
});
// 3. Check initial progress
const initialProgress = await contract.getProgress(testUser.address, achievementId);
console.log('Initial progress:', initialProgress);
// 4. Simulate activity (update progress)
await contract.updateProgress(testUser.address, achievementId, 100);
// 5. Verify can claim
const canClaim = await contract.canClaim(testUser.address, achievementId);
console.log('Can claim:', canClaim);
// 6. Claim achievement
const contractWithTestUser = contract.connect(testUser);
const tx = await contractWithTestUser.claimAchievement(achievementId, '0x');
await tx.wait();
console.log('✅ Achievement successfully claimed in test!');
}
Batch Achievement Creation
Creating Multiple Achievements
async function createAchievementSeries() {
const achievements = [
{
name: 'First Trade',
description: 'Complete your first trade',
category: 3,
rarity: 0,
points: 10,
requirement: { type: 'ACTIVITY_COUNT', count: 1 }
},
{
name: 'Active Trader',
description: 'Complete 10 trades',
category: 3,
rarity: 1,
points: 50,
requirement: { type: 'ACTIVITY_COUNT', count: 10 }
},
{
name: 'Trading Master',
description: 'Complete 100 trades',
category: 3,
rarity: 2,
points: 200,
requirement: { type: 'ACTIVITY_COUNT', count: 100 }
}
];
const createdIds = [];
for (const ach of achievements) {
const requirementData = ethers.utils.defaultAbiCoder.encode(
['bytes32', 'uint256'],
[ethers.utils.id('TRADE_COMPLETED'), ach.requirement.count]
);
const tx = await contract.createAchievement(
ach.name,
ach.description,
`ipfs://QmXyz.../${ach.name.toLowerCase().replace(/\s/g, '-')}.png`,
ach.category,
ach.rarity,
ach.points,
ach.points * 2, // 2x points as boost
3, // ACTIVITY_COUNT
requirementData
);
const receipt = await tx.wait();
const event = receipt.events.find(e => e.event === 'AchievementCreated');
createdIds.push(event.args.achievementId.toString());
console.log(`Created: ${ach.name} (ID: ${event.args.achievementId.toString()})`);
}
return createdIds;
}
Best Practices
Achievement Design
- Progressive Difficulty: Create series with increasing difficulty
- Clear Requirements: Make requirements obvious
- Balanced Rewards: Match rewards to difficulty
- Category Alignment: Ensure category matches activity
- Unique Names: Use descriptive, unique names
Implementation
- Test Thoroughly: Test on testnet first
- Monitor Events: Track creation and claim events
- Update Documentation: Document custom achievements
- User Communication: Announce new achievements
- Gather Feedback: Iterate based on user feedback
Verification
- Secure Verification: Protect verification endpoints
- Prevent Gaming: Implement anti-cheat measures
- Audit Logs: Maintain verification logs
- Regular Reviews: Review achievement completion
- Fair Distribution: Ensure fair access