Skip to content

feat: added Talent Protocol Builder Score #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,18 @@ Example query to Claude:

> "Buy $20 worth of OpenRouter credits."

### get_builder_score

Gets the builder score for a specific address.

Parameters:

- `builderAddress`: The address of the builder

Example query to Claude:

> "What is builder score for 0x5560650bF336dAfc2AaB23E6C2bCc78028877725?"

## Security Considerations

- The configuration file contains sensitive information (API keys and seed phrases). Ensure it's properly secured and not shared.
Expand Down
24 changes: 24 additions & 0 deletions examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,30 @@ _Claude uses the `buy_openrouter_credits` tool with the following parameters:_

**Claude:** I've initiated the purchase of $20 worth of OpenRouter credits using your USDC. The transaction hash is: 0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234

## Builder Score

### Get Builder Score

**You:** What is the builder score for 0x5560650bF336dAfc2AaB23E6C2bCc78028877725?

**Claude:** I'll check the builder score for that address.

_Claude uses the `get_talent_protocol_score` tool with the following parameters:_

- address: "0x5560650bF336dAfc2AaB23E6C2bCc78028877725"

**Claude:** I've retrieved the Builder Score for the address 0x5560650bF336dAfc2AaB23E6C2bCc78028877725:
Builder Score: 95 (Excellent)
This is the Talent Protocol profile for Harpreet Singh, who works as a Devrel at Rabble and identifies as a Full Stack Web3 Developer. Here are some additional details:

Activity Level: Low Activity
Social Presence: Connected to 6 platforms (basename, farcaster, github, lens, linkedin, twitter)
Total Followers: 1,363
GitHub: Username HAPPYS1NGH with 30 followers
Basename: happysingh.base.eth

The overall standing of "Excellent" suggests this is a well-established profile in the Web3 ecosystem.

## Tips for Effective Use

1. **Be specific in your requests** - Provide all necessary details when asking Claude to perform blockchain operations.
Expand Down
176 changes: 176 additions & 0 deletions src/tools/builder-score/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { isAddress } from 'viem';
import type { z } from 'zod';
import { GetBuilderScoreSchema, passportSchema } from './schemas.js';
import type { WalletClient, PublicActions } from 'viem';

export async function callBuilderScoreHandler(
wallet: WalletClient & PublicActions,
args: z.infer<typeof GetBuilderScoreSchema>,
): Promise<string> {
const { builderAddress } = args;

if (!isAddress(builderAddress, { strict: false })) {
throw new Error(`Invalid builder address: ${builderAddress}`);
}

const passport = await fetchBuilderPassport(
builderAddress,
);

if (!passport.success || !passport.data) {
throw new Error(`Failed to fetch passport: ${passport.error}`);
}

const analysisResult = analyzeBuilderData(passport.data);


return JSON.stringify({
builderAddress,
analysis: analysisResult,
});
}



async function fetchBuilderPassport(builderAddress: string) {
const TALENTPROTOCOL_API_KEY = process.env.TALENTPROTOCOL_API_KEY;
try {
if (!TALENTPROTOCOL_API_KEY) {
return {
success: false,
error: 'Talent Protocol API key not configured'
};
}

const url = `https://api.talentprotocol.com/api/v2/passports/${builderAddress}`;

const response = await fetch(url, {
method: 'GET',
headers: {
'x-api-key': TALENTPROTOCOL_API_KEY,
'Content-Type': 'application/json',
},
});

if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: `API error (${response.status}): ${errorText}`
};
}

const data = await response.json();

// Validate the response against our schema
try {
const validatedData = passportSchema.parse(data);
return { success: true, data: validatedData };
} catch (validationError) {
console.error('API response validation error:', validationError);
return {
success: false,
error: 'Invalid API response format'
};
}

} catch (error) {
console.error('Error fetching builder passport:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}


function analyzeBuilderData(data: any) {
const passport = data.passport;

// Calculate overall standing based on scores
let overallStanding = 'Unknown';
if (passport.score !== undefined) {
if (passport.score >= 80) overallStanding = 'Excellent';
else if (passport.score >= 60) overallStanding = 'Good';
else if (passport.score >= 40) overallStanding = 'Average';
else overallStanding = 'Developing';
}

// Check verification status
const isVerified = passport.human_checkmark === true || passport.verified === true;

// Check activity level
let activityLevel = 'Unknown';
if (passport.activity_score !== undefined) {
if (passport.activity_score >= 80) activityLevel = 'Very Active';
else if (passport.activity_score >= 50) activityLevel = 'Active';
else activityLevel = 'Low Activity';
}

// Get skills/tags
const skills = passport.passport_profile?.tags || [];

const socialProfiles = passport.passport_socials || [];
const connectedPlatforms = socialProfiles.map((social: any) => social.source);

// Specifically check for GitHub presence
const githubProfile = socialProfiles.find((social: any) => social.source === 'github');
const hasGithub = !!githubProfile;

// Check for basename presence
const basenameProfile = socialProfiles.find((social: any) => social.source === 'basename');
const hasBasename = !!basenameProfile;

// Get total followers across platforms
let totalFollowers = 0;
socialProfiles.forEach((social: any) => {
if (social.follower_count && typeof social.follower_count === 'number') {
totalFollowers += social.follower_count;
}
});

// Get display name and bio from profile or socials
const displayName = passport.passport_profile?.display_name ||
passport.user?.name ||
socialProfiles[0]?.profile_display_name ||
'Unknown';

const bio = passport.passport_profile?.bio ||
socialProfiles.find((social: any) => social.profile_bio)?.profile_bio ||
'';

return {
builderScore: passport.score,
name: displayName,
bio: bio,

overallStanding,
isVerified,
activityLevel,
skills,

socialPresence: {
totalFollowers,
connectedPlatforms,
platformCount: connectedPlatforms.length
},

github: hasGithub ? {
present: true,
username: githubProfile.profile_name,
followers: githubProfile.follower_count,
following: githubProfile.following_count,
profileUrl: githubProfile.profile_url
} : {
present: false
},

basename: hasBasename ? {
present: true,
name: basenameProfile.profile_name,
profileUrl: basenameProfile.profile_url
} : {
present: false
}
};
}
10 changes: 10 additions & 0 deletions src/tools/builder-score/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { generateTool } from '../../utils.js';
import { callBuilderScoreHandler } from './handlers.js';
import { GetBuilderScoreSchema } from './schemas.js';

export const callBuilderScoreTool = generateTool({
name: 'get_builder_score',
description: 'Get the Talent Protocol Builder Score for a specific address',
inputSchema: GetBuilderScoreSchema,
toolHandler: callBuilderScoreHandler,
});
86 changes: 86 additions & 0 deletions src/tools/builder-score/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { z } from 'zod';

// Schema for validating incoming requests
export const GetBuilderScoreSchema = z.object({
builderAddress: z.string()
.min(1, "Builder address is required")
.regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format"),
});


// Schema for data sources in passport profile
const dataSourcesSchema = z.object({
profile_bio: z.string().optional(),
profile_name: z.string().optional(),
profile_image_url: z.string().optional(),
profile_display_name: z.string().optional(),
}).optional();

// Schema for passport profile data
const passportProfileSchema = z.object({
bio: z.string().optional(),
data_sources: dataSourcesSchema,
display_name: z.string().optional(),
image_url: z.string().optional(),
location: z.string().nullable(),
name: z.string().optional(),
tags: z.array(z.string()).optional(),
});

// Schema for passport social data
const passportSocialSchema = z.object({
disconnected: z.boolean().optional(),
follower_count: z.number().nullable(),
following_count: z.number().nullable(),
location: z.string().nullable(),
profile_bio: z.string().optional(),
profile_display_name: z.string().optional(),
profile_image_url: z.string().nullable(),
profile_name: z.string().optional(),
profile_url: z.string().optional(),
source: z.string().optional(),
});

// Schema for user data
const userSchema = z.object({
admin: z.boolean().optional(),
email: z.string().nullable(),
id: z.string().optional(),
name: z.string().optional(),
profile_picture_url: z.string().optional(),
});

// Schema for the passport data returned from Talent Protocol API
export const passportSchema = z.object({
passport: z.object({
activity_score: z.number().optional(),
calculating_score: z.boolean().optional(),
created_at: z.string().optional(),
human_checkmark: z.boolean().optional(),
identity_score: z.number().optional(),
last_calculated_at: z.string().optional(),
main_wallet: z.string().optional(),
main_wallet_changed_at: z.string().nullable(),
merged: z.boolean().optional(),
migrated_v2_at: z.string().optional(),
nominations_received_count: z.number().optional(),
onchain: z.boolean().optional(),
passport_id: z.number().optional(),
passport_profile: passportProfileSchema.optional(),
passport_socials: z.array(passportSocialSchema).optional(),
pending_kyc: z.boolean().optional(),
score: z.number().optional(),
skills_score: z.number().optional(),
socials_calculated_at: z.string().optional(),
user: userSchema.optional(),
verified: z.boolean().optional(),
verified_wallets: z.array(z.string()).optional(),
}),
});

// Response schema for our API
export const responseSchema = z.object({
message: z.string(),
builderAddress: z.string(),
builderData: passportSchema,
});
2 changes: 2 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { erc20BalanceTool, erc20TransferTool } from './erc20/index.js';
import { getMorphoVaultsTool } from './morpho/index.js';
import { getOnrampAssetsTool, onrampTool } from './onramp/index.js';
import { buyOpenRouterCreditsTool } from './open-router/index.js';
import { callBuilderScoreTool } from './builder-score/index.js';
import type { ToolHandler, ToolWithHandler } from './types.js';

export const baseMcpTools: ToolWithHandler[] = [
callBuilderScoreTool,
getMorphoVaultsTool,
callContractTool,
getOnrampAssetsTool,
Expand Down