diff --git a/backend/MCP_Execution_Server/package-lock.json b/backend/MCP_Execution_Server/package-lock.json
index 07c9e46..53dfedb 100644
--- a/backend/MCP_Execution_Server/package-lock.json
+++ b/backend/MCP_Execution_Server/package-lock.json
@@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"@chainsafe/libp2p-gossipsub": "^14.1.0",
+ "@google/genai": "^0.13.0",
"@libp2p/identify": "^3.0.27",
"@libp2p/tcp": "^10.1.8",
"@modelcontextprotocol/sdk": "^1.7.0",
@@ -1251,6 +1252,21 @@
"@ethersproject/strings": "^5.8.0"
}
},
+ "node_modules/@google/genai": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.13.0.tgz",
+ "integrity": "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "google-auth-library": "^9.14.2",
+ "ws": "^8.18.0",
+ "zod": "^3.22.4",
+ "zod-to-json-schema": "^3.22.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@iden3/bigarray": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@iden3/bigarray/-/bigarray-0.0.2.tgz",
@@ -3395,6 +3411,12 @@
"ieee754": "^1.2.1"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -4250,6 +4272,15 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -4837,6 +4868,12 @@
"node": ">= 18"
}
},
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5152,6 +5189,36 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/gaxios": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+ "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "is-stream": "^2.0.0",
+ "node-fetch": "^2.6.9",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gcp-metadata": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
+ "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "gaxios": "^6.1.1",
+ "google-logging-utils": "^0.0.2",
+ "json-bigint": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -5280,6 +5347,32 @@
"node": ">=4"
}
},
+ "node_modules/google-auth-library": {
+ "version": "9.15.1",
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
+ "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "ecdsa-sig-formatter": "^1.0.11",
+ "gaxios": "^6.1.1",
+ "gcp-metadata": "^6.1.0",
+ "gtoken": "^7.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/google-logging-utils": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
+ "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -5298,6 +5391,19 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
+ "node_modules/gtoken": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
+ "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
+ "license": "MIT",
+ "dependencies": {
+ "gaxios": "^6.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/hard-rejection": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
@@ -6373,6 +6479,27 @@
"node": "*"
}
},
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+ "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.0",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -7276,6 +7403,26 @@
"integrity": "sha512-+I10J3wKNoKddNxn0CNpoZ3eTZuqxjNM3b1GImVx22+ePI+Y15P8g/j3WsbP0fhzzrFzrtjOAoq5NCCucswXOQ==",
"license": "MIT"
},
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/node-gyp": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz",
diff --git a/backend/MCP_Execution_Server/package.json b/backend/MCP_Execution_Server/package.json
index 65bc559..7ac07d9 100644
--- a/backend/MCP_Execution_Server/package.json
+++ b/backend/MCP_Execution_Server/package.json
@@ -17,6 +17,7 @@
"description": "",
"dependencies": {
"@chainsafe/libp2p-gossipsub": "^14.1.0",
+ "@google/genai": "^0.13.0",
"@libp2p/identify": "^3.0.27",
"@libp2p/tcp": "^10.1.8",
"@modelcontextprotocol/sdk": "^1.7.0",
@@ -40,4 +41,4 @@
"nodemon": "^3.1.0",
"typescript": "^5.8.2"
}
-}
\ No newline at end of file
+}
diff --git a/backend/MCP_Execution_Server/src/config.ts b/backend/MCP_Execution_Server/src/config.ts
index 3161243..9624a1b 100644
--- a/backend/MCP_Execution_Server/src/config.ts
+++ b/backend/MCP_Execution_Server/src/config.ts
@@ -10,6 +10,9 @@ export const config = {
apiKey: process.env.NEYNAR_API_KEY ?? "0x",
signerUuid: process.env.NEYNAR_SIGNER_UUID ?? "0x",
},
+ gemini:{
+ apiKey: process.env.GEMINI_API_KEY ?? "0x",
+ },
openai: {
apiKey: process.env.OPENAI_API_KEY,
},
diff --git a/backend/MCP_Execution_Server/src/index.ts b/backend/MCP_Execution_Server/src/index.ts
index 2e8f050..b8a7d66 100644
--- a/backend/MCP_Execution_Server/src/index.ts
+++ b/backend/MCP_Execution_Server/src/index.ts
@@ -1,4 +1,4 @@
-import { ReclaimClient } from '@reclaimprotocol/zk-fetch';
+import { ReclaimClient } from "@reclaimprotocol/zk-fetch";
import { config } from "./config.js";
@@ -9,25 +9,38 @@ import { UserService } from "./services/user.service.js";
import { AVSService } from "./services/avs.service.js";
import { IpfsService } from "./services/ipfs.service.js";
import { DBService } from "./services/db.service.js";
+import { GeminiAiService } from "./services/gemini_ai.service.js";
/**
* Main application entry point
*/
async function main() {
- const db = new DBService(config.supabase.SUPABASE_URL, config.supabase.SUPABASE_ANON_KEY);
+ const db = new DBService(
+ config.supabase.SUPABASE_URL,
+ config.supabase.SUPABASE_ANON_KEY,
+ );
const reclaim = new ReclaimClient(
config.reclaim.appId,
config.reclaim.appSecret,
- )
-
+ );
+ const geminiai = new GeminiAiService(config.gemini.apiKey as string);
const ai = new AIService(config.openai.apiKey as string);
const avs = new AVSService(
config.network.rpcBaseAddress,
config.network.privateKey,
);
- const ipfs = new IpfsService(config.pinata.apiKey, config.pinata.secretApiKey);
- const neynar = new NeynarService(config.neynar.apiKey, config.neynar.signerUuid, reclaim, avs, ipfs);
+ const ipfs = new IpfsService(
+ config.pinata.apiKey,
+ config.pinata.secretApiKey,
+ );
+ const neynar = new NeynarService(
+ config.neynar.apiKey,
+ config.neynar.signerUuid,
+ reclaim,
+ avs,
+ ipfs,
+ );
const user = new UserService(neynar, ai, db);
// Initialize services
@@ -38,13 +51,13 @@ async function main() {
reclaim,
ipfs,
avs,
+ geminiai,
};
const executionServer = new ExecutionServer(config, services);
try {
// Create and start server
await executionServer.start();
-
} catch (error) {
console.error("Fatal error in main():", error);
process.exit(1);
diff --git a/backend/MCP_Execution_Server/src/services/gemini_ai.service.ts b/backend/MCP_Execution_Server/src/services/gemini_ai.service.ts
new file mode 100644
index 0000000..0b80a88
--- /dev/null
+++ b/backend/MCP_Execution_Server/src/services/gemini_ai.service.ts
@@ -0,0 +1,216 @@
+import { GoogleGenAI } from "@google/genai";
+
+export class GeminiAiService {
+ private ai: GoogleGenAI;
+ private geminiModel: string;
+ private embeddingModel: string;
+
+ constructor(private geminiApiKey: string) {
+ this.ai = new GoogleGenAI({
+ apiKey: this.geminiApiKey,
+ });
+ this.geminiModel = "gemini-2.5-flash";
+ this.embeddingModel = "embedding-001";
+ }
+
+ async summarizeUserContext(userData: any) {
+ const prompt = `
+
+From the user's Farcaster data, extract a structured keyword profile representing their interests, behaviors, communities, and preferences.
+This profile will be used to generate embeddings and cluster users into highly relevant cohorts.
+
+
+
+${JSON.stringify(userData, null, 2)}
+
+
+
+1. Return a flat list of lowercase, hyphenated keywords (no sentences).
+2. Each keyword should reflect a meaningful trait, behavior, tool, or interest (e.g. 'zora-user', 'frame-builder', 'philosophy-discussions', 'onchain-gaming').
+3. Focus on cohort-defining dimensions: content topics, communities, actions, personality style, and engagement type.
+4. Avoid vague terms like "web3" or "crypto" unless combined with specificity (e.g. 'web3-design', 'crypto-security-research').
+5. No filler words, no explanation — only the keyword list, comma-separated.
+
+
+
+Good output:
+"frame-builder, farcaster-poweruser, zora-poster, thoughtful-replier, ai-curious, defi-scalability, ethcc-attendee, builder-in-public, photography-enthusiast, governance-participant"
+
+Bad output:
+"This user enjoys Web3 and tech. They are very active online and like to post." (Too vague, narrative style)
+
+ `;
+
+ try {
+ const result = await this.ai.models.generateContent({
+ model: this.geminiModel,
+ contents: prompt,
+ });
+ return result.text;
+ } catch (err: any) {
+ console.error("summarizeUserContext error", err);
+ return null;
+ }
+ }
+
+ async findMeaningFromText(text: string) {
+ const prompt = `
+
+Analyze the following Farcaster cast and extract a flat, structured list of cohort-relevant keywords.
+These keywords will be used for user clustering, so focus on identifying meaningful interests, behaviors, communities, and intent expressed in the cast.
+
+
+
+${text}
+
+
+
+1. Return a flat list of lowercase, hyphenated keywords (no sentences or explanations).
+2. Each keyword should reflect a cohort-relevant dimension such as:
+ - Specific interest/topic (e.g. 'onchain-fitness', 'ai-art-tools')
+ - Behavioral pattern or intent (e.g. 'open-collab-invite', 'builder-outreach')
+ - Community or context (e.g. 'farcaster-networking', 'zora-poster')
+ - Product or content domain (e.g. 'fitness-dapp-creator', 'frame-developer')
+ - Personality or engagement style (e.g. 'thoughtful-replier', 'public-builder')
+3. Avoid vague terms like 'web3', 'tech', or generic verbs.
+4. Keep it concise — no more than 10 keywords per cast.
+5. Output must be a single comma-separated line of keywords only.
+
+
+
+Input: "building something around onchain-fitness — let's connect?"
+Output: onchain-fitness, builder-outreach, farcaster-networking, fitness-dapp-creator, open-collab-invite, community-collaborator, health-and-wellness
+
+Input: "launched a new frame using Zora — supports music NFTs"
+Output: zora-frame-builder, music-nft-creator, frame-launcher, zora-user, creative-tools-user, public-release-announcement
+
+ `;
+
+ try {
+ const result = await this.ai.models.generateContent({
+ model: this.geminiModel,
+ contents: prompt,
+ });
+ return result.text;
+ } catch (err: any) {
+ console.error("findMeaningFromText error", err);
+ return null;
+ }
+ }
+
+ async generateReplyForCast({
+ userCast,
+ castSummary,
+ similarUserFeeds,
+ trendingFeeds,
+ }: {
+ userCast: string;
+ castSummary: string;
+ similarUserFeeds: any[];
+ trendingFeeds: any[];
+ }) {
+ const formattedTrendingFeeds = trendingFeeds
+ .map((feed) => JSON.stringify(feed))
+ .join("\n");
+ const formattedSimilarUserFeeds = similarUserFeeds
+ .map((feed) => JSON.stringify(feed))
+ .join("\n");
+
+ const prompt = `
+
+Analyze the user's cast and prioritize finding relevant content from similar users first, then trending feeds if necessary. Select the SINGLE most relevant cast that connects with the user's interests or topic. Only select from the actual provided content - never generate or fabricate anything.
+
+
+
+${userCast}
+
+
+Use the cast summary to understand the user's interests and context, this will provide a deeper understanding of the user's intent and preferences.
+
+${castSummary}
+
+
+
+${formattedSimilarUserFeeds}
+
+
+
+${formattedTrendingFeeds}
+
+
+
+1. First identify key themes, topics, and interests in the user's cast.
+2. PRIORITIZE similar user feeds - examine these first and try to find the most relevant match here before looking at trending feeds.
+3. Only if no good match exists in similar user feeds, then examine the trending feeds.
+4. Select only ONE existing cast that best relates to the user's content based on:
+ - Topic relevance
+ - Semantic similarity
+ - Any shared channels or interests
+5. If no relevant casts are found or if all feeds are malformed, respond with: "No relevant trending casts found in the provided data."
+6. Never fabricate or generate casts - only select from what is actually provided in the similar_user_feeds or trending_feeds.
+7. AVOID talking about airdrops and giveaways.
+
+
+
+You MUST respond with ONLY a valid JSON object containing exactly these two fields:
+- replyText: A string with the message "You should connect with [author_username], who said: '[cast_text]'" - Max 60-70 words, 320 characters.
+- link: A string with the URL "https://warpcast.com/[author_username]/[cast_hash]" - this should MATCH the cast you selected and not a fabricated/random one.
+
+If a channel exists, append "Join the conversation in the /[channel_name] channel." to the replyText value.
+
+Example output:
+{
+ "replyText": "You should connect with username123, who said: 'This is an interesting thought about AI.' Join the conversation in the /ai channel.",
+ "link": "https://warpcast.com/username123/0x123abc"
+}
+
+If and only if you find no relevant casts, respond with this exact format:
+{
+ "replyText": "No relevant trending casts found in the provided data.",
+ "link": ""
+}
+
+
+
+Your response should be a valid JSON object with no additional text or explanation.
+
+ `;
+
+ try {
+ const result = await this.ai.models.generateContent({
+ model: this.geminiModel,
+ contents: prompt,
+ });
+ const content = result.text?.trim();
+
+ try {
+ return content
+ ? JSON.parse(content)
+ : {
+ replyText:
+ "No relevant trending casts found in the provided data.",
+ link: "",
+ };
+ } catch (jsonErr) {
+ console.warn("Response was not valid JSON:", content);
+ return { error: "Invalid JSON format in AI response", raw: content };
+ }
+ } catch (err: any) {
+ console.error("generateReplyForCast error", err);
+ return "Sorry, I couldn't generate a reply at the moment.";
+ }
+ }
+
+ async generateEmbeddings(text: string) {
+ try {
+ const result = await this.ai.models.embedContent({
+ model: this.embeddingModel,
+ contents: text,
+ });
+ return result.embeddings;
+ } catch (err: any) {
+ console.error("generateEmbeddings error", err);
+ return null;
+ }
+ }
+}
diff --git a/backend/MCP_Execution_Server/src/services/user.service.ts b/backend/MCP_Execution_Server/src/services/user.service.ts
index 95d7920..2abc07b 100644
--- a/backend/MCP_Execution_Server/src/services/user.service.ts
+++ b/backend/MCP_Execution_Server/src/services/user.service.ts
@@ -1,6 +1,7 @@
import type { NeynarService } from "./neynar.service.js";
import type { AIService } from "./ai.service.js";
import type { DBService } from "./db.service.js";
+import { GeminiAiService } from "./gemini_ai.service.js";
export enum FID_STATUS {
NOT_EXIST = "NOT_EXIST",
@@ -13,8 +14,67 @@ export class UserService {
private neynarService: NeynarService,
private aiService: AIService,
private db: DBService,
- ) { }
+ private geminiService?: GeminiAiService,
+ ) {}
+ private async summarizeUserContextSafe(
+ userData: any,
+ ): Promise {
+ try {
+ if (this.geminiService) {
+ const summary = await this.geminiService.summarizeUserContext(userData);
+ if (summary) return summary;
+ }
+ return await this.aiService.summarizeUserContext(userData);
+ } catch (e) {
+ console.warn("Fallback to AIService for summarizeUserContext");
+ return await this.aiService.summarizeUserContext(userData);
+ }
+ }
+
+ private async generateEmbeddingsSafe(text: string): Promise {
+ try {
+ if (this.geminiService) {
+ const embeddings = await this.geminiService.generateEmbeddings(text);
+ if (embeddings) return embeddings;
+ }
+ return await this.aiService.generateEmbeddings(text);
+ } catch (e) {
+ console.warn("Fallback to AIService for generateEmbeddings");
+ return await this.aiService.generateEmbeddings(text);
+ }
+ }
+
+ private async generateReplyForCastSafe(input: {
+ userCast: string;
+ castSummary: string;
+ similarUserFeeds: any;
+ trendingFeeds: any;
+ }): Promise {
+ try {
+ if (this.geminiService) {
+ const reply = await this.geminiService.generateReplyForCast(input);
+ if (reply?.replyText) return reply;
+ }
+ return await this.aiService.generateReplyForCast(input);
+ } catch (e) {
+ console.warn("Fallback to AIService for generateReplyForCast");
+ return await this.aiService.generateReplyForCast(input);
+ }
+ }
+
+ private async findMeaningFromTextSafe(text: string): Promise {
+ try {
+ if (this.geminiService) {
+ const summary = await this.geminiService.findMeaningFromText(text);
+ if (summary) return summary;
+ }
+ return await this.aiService.findMeaningFromText(text);
+ } catch (e) {
+ console.warn("Fallback to AIService for findMeaningFromText");
+ return await this.aiService.findMeaningFromText(text);
+ }
+ }
async fetchAllUsers() {
try {
const { success, data } = await this.db.fetchAllFIDs();
@@ -25,7 +85,7 @@ export class UserService {
return { success: false, error: err.message || err };
}
}
-
+
async unsubscribeUser(fid: number) {
try {
const { success, data } = await this.db.unsubscribeFID(fid);
@@ -49,7 +109,9 @@ export class UserService {
}
}
- async checkFIDStatus(fid: number): Promise<{ success: boolean; status?: FID_STATUS; error?: string }> {
+ async checkFIDStatus(
+ fid: number,
+ ): Promise<{ success: boolean; status?: FID_STATUS; error?: string }> {
try {
const { success, data } = await this.db.checkFIDStatus(fid);
@@ -64,20 +126,21 @@ export class UserService {
return { success: true, status: FID_STATUS.EXIST };
} catch (err: unknown) {
console.error("checkFIDStatus error", err);
- return { success: false, error: err instanceof Error ? err.message : String(err) };
+ return {
+ success: false,
+ error: err instanceof Error ? err.message : String(err),
+ };
}
}
async registerUser(fid: string) {
try {
-
- const { success, data: alreadySubscribedFIDs } = await this.db.fetchSubscribedFIDs();
+ const { success, data: alreadySubscribedFIDs } =
+ await this.db.fetchSubscribedFIDs();
if (!success) throw new Error("Failed to fetch subscribed FIDs");
-
const alreadySubsribed = await this.checkFIDStatus(Number(fid));
if (alreadySubsribed.success) {
-
if (alreadySubsribed.status === FID_STATUS.SUBSCRIBED) {
return { success: true, data: `User ${fid} already subscribed` };
}
@@ -93,12 +156,16 @@ export class UserService {
}
const userData = await this.neynarService.aggregateUserData(fid);
- const summary = await this.aiService.summarizeUserContext(userData);
+ const summary = await this.summarizeUserContextSafe(userData);
if (!summary) throw new Error("Summary generation failed");
- const embeddings = await this.aiService.generateEmbeddings(summary);
+ const embeddings = await this.generateEmbeddingsSafe(summary);
if (!embeddings) throw new Error("Embedding generation failed");
- const { success } = await this.db.registerAndSubscribeFID(fid, summary, embeddings);
+ const { success } = await this.db.registerAndSubscribeFID(
+ fid,
+ summary,
+ embeddings,
+ );
if (!success) throw new Error("User registration failed");
const newSubscribedUserIds = [...alreadySubscribedFIDs, fid];
@@ -135,7 +202,11 @@ export class UserService {
const embeddings = await this.aiService.generateEmbeddings(summary);
if (!embeddings) throw new Error("Embedding generation failed");
- const { success } = await this.db.onlyRegisterFID(fid, summary, embeddings);
+ const { success } = await this.db.onlyRegisterFID(
+ fid,
+ summary,
+ embeddings,
+ );
console.log("Registered user data", fid);
if (!success) throw new Error("User registration failed");
return { success: true, data: userData };
@@ -180,21 +251,21 @@ export class UserService {
try {
// Step 1: Check if the DB has the FID of the user who sent the webhook
- const { success, registered } = await this.db.isRegistered(Number(fid));
+ // const { success, registered } = await this.db.isRegistered(Number(fid));
- if (!success || !registered) {
- throw new Error(`User with fid ${fid} not found`);
- }
+ // if (!success || !registered) {
+ // throw new Error(`User with fid ${fid} not found`);
+ // }
// Step 2: Generate embeddings for the received cast
- const castSummary = await this.aiService.findMeaningFromText(cast.text);
- const castEmbeddings =
- await this.aiService.generateEmbeddings(castSummary);
+ const castSummary = await this.findMeaningFromTextSafe(cast.text);
+ const castEmbeddings = await this.generateEmbeddingsSafe(castSummary);
if (!castEmbeddings) {
throw new Error("Embedding generation failed for the cast text");
}
- const { data: similarUsers, error: similarityError } = await this.db.fetchSimilarFIDs(castEmbeddings, 0.4, 3);
+ const { data: similarUsers, error: similarityError } =
+ await this.db.fetchSimilarFIDs(castEmbeddings, 0.4, 3);
console.log("Similar users", similarUsers);
if (similarityError || !similarUsers) {
throw new Error("Error finding similar users");
@@ -219,14 +290,15 @@ export class UserService {
);
const similarUserFeeds = await Promise.all(userFeedPromises);
const trendingFeeds = await this.neynarService.fetchTrendingFeeds();
+ console.log("Trending feeds", trendingFeeds);
- const aiResponse = await this.aiService.generateReplyForCast({
+ const aiResponse = await this.generateReplyForCastSafe({
userCast: cast.text,
- castSummary: castSummary,
+ castSummary,
similarUserFeeds,
trendingFeeds,
});
-
+ console.log("AI response", aiResponse);
if (!aiResponse || !aiResponse.replyText) {
throw new Error("AI response generation failed");
}
@@ -239,21 +311,21 @@ export class UserService {
return { success: true, data: aiResponse };
}
- const castReply = await this.neynarService.replyToCast({
- text: aiResponse.replyText,
- parentHash: cast.hash,
- embeds: [
- {
- url: aiResponse.link,
- },
- ],
- });
-
- console.log("Cast replied");
-
- await this.db.addCastReply(cast.hash);
-
- return { success: true, data: castReply };
+ // const castReply = await this.neynarService.replyToCast({
+ // text: aiResponse.replyText,
+ // parentHash: cast.hash,
+ // embeds: [
+ // {
+ // url: aiResponse.link,
+ // },
+ // ],
+ // });
+ //
+ // console.log("Cast replied");
+
+ // await this.db.addCastReply(cast.hash);
+
+ return { success: true, data: aiResponse};
} catch (err: any) {
console.error("registerCast error", err);
return { success: false, error: err.message || err };
diff --git a/backend/MCP_Execution_Server/src/tools/register-user.ts b/backend/MCP_Execution_Server/src/tools/register-user.ts
index 76893fa..1212c80 100644
--- a/backend/MCP_Execution_Server/src/tools/register-user.ts
+++ b/backend/MCP_Execution_Server/src/tools/register-user.ts
@@ -10,111 +10,127 @@ import { DBService } from "../services/db.service.js";
import { config } from "../config.js";
import { ReclaimClient } from "@reclaimprotocol/zk-fetch";
+import { GeminiAiService } from "../services/gemini_ai.service.js";
/**
* CLI tool to register a user's data by FID
*/
async function registerUser() {
- const db = new DBService(config.supabase.SUPABASE_URL, config.supabase.SUPABASE_ANON_KEY);
-
- const reclaim = new ReclaimClient(
- config.reclaim.appId,
- config.reclaim.appSecret,
- )
-
- const ai = new AIService(config.openai.apiKey as string);
- console.log("RPC BASE ADDRESS:", config.network.rpcBaseAddress);
- const avs = new AVSService(
- config.network.rpcBaseAddress,
- config.network.privateKey,
+ const db = new DBService(
+ config.supabase.SUPABASE_URL,
+ config.supabase.SUPABASE_ANON_KEY,
+ );
+
+ const reclaim = new ReclaimClient(
+ config.reclaim.appId,
+ config.reclaim.appSecret,
+ );
+
+ const ai = new AIService(config.openai.apiKey as string);
+ const gemini = new GeminiAiService(config.gemini.apiKey as string);
+ console.log("RPC BASE ADDRESS:", config.network.rpcBaseAddress);
+ const avs = new AVSService(
+ config.network.rpcBaseAddress,
+ config.network.privateKey,
+ );
+ const ipfs = new IpfsService(
+ config.pinata.apiKey,
+ config.pinata.secretApiKey,
+ );
+ const neynar = new NeynarService(
+ config.neynar.apiKey,
+ config.neynar.signerUuid,
+ reclaim,
+ avs,
+ ipfs,
+ );
+ const userService = new UserService(neynar, ai, db,gemini);
+ /**
+ * Channel Fetched
+ * Nomads
+ * Monad
+ * Screens
+ * f1
+ * books
+ * ai
+ * creators
+ * crypto
+ * geopolitics
+ * fitness
+ * farcaster
+ */
+
+ const channelId = "degen";
+
+ // Define the response type based on the Farcaster API docs
+ interface ChannelMember {
+ fid: number;
+ memberAt: number;
+ }
+
+ interface ChannelMembersResponse {
+ result: {
+ members: ChannelMember[];
+ };
+ next?: {
+ cursor: string;
+ };
+ }
+
+ let members: ChannelMember[] = [];
+
+ try {
+ // First API call to get initial members
+ const initialResponse = await axios.get(
+ `https://api.warpcast.com/fc/channel-members?channelId=${channelId}`,
);
- const ipfs = new IpfsService(config.pinata.apiKey, config.pinata.secretApiKey);
- const neynar = new NeynarService(config.neynar.apiKey, config.neynar.signerUuid, reclaim, avs, ipfs);
- const userService = new UserService(neynar, ai, db);
- /**
- * Channel Fetched
- * Nomads
- * Monad
- * Screens
- * f1
- * books
- * ai
- * creators
- * crypto
- * geopolitics
- * fitness
- * farcaster
- */
-
- const channelId = "degen";
-
- // Define the response type based on the Farcaster API docs
- interface ChannelMember {
- fid: number;
- memberAt: number;
- }
- interface ChannelMembersResponse {
- result: {
- members: ChannelMember[];
- };
- next?: {
- cursor: string;
- };
+ // Add initial members to our array
+ if (initialResponse.data?.result?.members) {
+ members = [...initialResponse.data.result.members];
}
- let members: ChannelMember[] = [];
+ // Handle pagination if there's a next cursor
+ if (initialResponse.data?.next?.cursor) {
+ let cursor: string | undefined = initialResponse.data.next.cursor;
- try {
- // First API call to get initial members
- const initialResponse = await axios.get(
- `https://api.warpcast.com/fc/channel-members?channelId=${channelId}`
- );
-
- // Add initial members to our array
- if (initialResponse.data?.result?.members) {
- members = [...initialResponse.data.result.members];
- }
+ // Continue fetching pages while there's a cursor
+ while (cursor) {
+ const nextResponse: AxiosResponse =
+ await axios.get(
+ `https://api.warpcast.com/fc/channel-members?channelId=${channelId}&cursor=${cursor}`,
+ );
- // Handle pagination if there's a next cursor
- if (initialResponse.data?.next?.cursor) {
- let cursor: string | undefined = initialResponse.data.next.cursor;
-
- // Continue fetching pages while there's a cursor
- while (cursor) {
- const nextResponse: AxiosResponse = await axios.get(
- `https://api.warpcast.com/fc/channel-members?channelId=${channelId}&cursor=${cursor}`
- );
-
- if (nextResponse.data?.result?.members) {
- members.push(...nextResponse.data.result.members);
- }
-
- // Update cursor for next page or exit loop
- cursor = nextResponse.data?.next?.cursor;
- }
- } else {
- console.log("Only one page of results found");
+ if (nextResponse.data?.result?.members) {
+ members.push(...nextResponse.data.result.members);
}
- } catch (error) {
- console.error("Error occurred fetching channel members:", error);
- return; // Exit early if we can't fetch the members
+ // Update cursor for next page or exit loop
+ cursor = nextResponse.data?.next?.cursor;
+ }
+ } else {
+ console.log("Only one page of results found");
}
+ } catch (error) {
+ console.error("Error occurred fetching channel members:", error);
+ return; // Exit early if we can't fetch the members
+ }
- const fids = members.map((member: ChannelMember) => member.fid);
- console.log(`Total number of users for ${channelId} to register: ${fids.length}`);
+ const fids = members.map((member: ChannelMember) => member.fid);
+ console.log(
+ `Total number of users for ${channelId} to register: ${fids.length}`,
+ );
- for (const fid of fids) {
- try {
- await userService.registerUserDataForBackend(fid.toString());
- } catch (error) {
- console.error(`Error registering user with FID ${fid}:`, error);
- }
+ for (const fid of fids) {
+ try {
+ await userService.registerUserDataForBackend(fid.toString());
+ } catch (error) {
+ console.error(`Error registering user with FID ${fid}:`, error);
}
- console.log("Registered all users in the channel:", channelId);
+ }
+ console.log("Registered all users in the channel:", channelId);
- return;
+ return;
}
// Run the script
-registerUser();
\ No newline at end of file
+registerUser();