diff --git a/README.md b/README.md index 1f7a3db..b84c05a 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/examples.md b/examples.md index 3f150c4..f87ce8f 100644 --- a/examples.md +++ b/examples.md @@ -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. diff --git a/src/tools/builder-score/handlers.ts b/src/tools/builder-score/handlers.ts new file mode 100644 index 0000000..adcb060 --- /dev/null +++ b/src/tools/builder-score/handlers.ts @@ -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, +): Promise { + 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 + } + }; +} diff --git a/src/tools/builder-score/index.ts b/src/tools/builder-score/index.ts new file mode 100644 index 0000000..e15d5d9 --- /dev/null +++ b/src/tools/builder-score/index.ts @@ -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, +}); diff --git a/src/tools/builder-score/schemas.ts b/src/tools/builder-score/schemas.ts new file mode 100644 index 0000000..59f1ecc --- /dev/null +++ b/src/tools/builder-score/schemas.ts @@ -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, +}); diff --git a/src/tools/index.ts b/src/tools/index.ts index b254d32..0130ee7 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -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,