Skip to content

casteragents/Caster-Agents-Framework-V2

Repository files navigation

Below is a complete README.md file for the Caster Agents Framework V2, followed by instructions on how to use it and the necessary files to get started. This guide will walk you through downloading the code, setting it up, and running the framework step-by-step, even if you're new to this process.


README.md

Caster Agents Framework V2

Overview

The Caster Agents Framework V2 is an open-source tool designed to build AI-powered agents for Farcaster. These agents can respond to mentions, like posts, recast content, follow users, post to channels, transfer tokens, and deploy Clanker tokens. It uses the Neynar API v2 for Farcaster interactions, OpenAI for AI responses, and ethers.js for blockchain operations.

This README provides a beginner-friendly guide to download, set up, and run the framework.

Features

  • Replies to mentions with AI-generated responses.
  • Likes, recasts, and quote-casts posts based on configurable probabilities.
  • Follows users who interact with the agent.
  • Posts to a specified channel every 20 minutes.
  • Transfers tokens (e.g., $Saindy) to users who mention the agent.
  • Deploys Clanker tokens when users request it via "deploy clanker" in a mention.

Prerequisites

Before starting, ensure you have:

  • Node.js (version 14 or higher): Download from nodejs.org.
  • Git: Download from git-scm.com to clone the repository.
  • Internet access: Required for API calls.
  • Credentials (you’ll need these later):
    • Farcaster Signer UUID
    • Neynar API Key
    • OpenAI API Key
    • Your Farcaster FID
    • Clanker API Key (optional, for Clanker token deployment)
    • Token contract address (for token transfers)
    • Agent’s private key (for wallet operations)

Step-by-Step Setup Guide

1. Clone the Repository

To download the code, you’ll clone the repository from GitHub. Follow these steps:

  1. Open your terminal:
    • Windows: Search for "Command Prompt" or "PowerShell".
    • macOS/Linux: Open "Terminal".
  2. Run this command to clone the repository (replace the URL with the actual repository URL if you’re using a specific one):
    git clone https://github.com/casteragents/Caster-Agents-Framework-V2.git
  3. Navigate into the project folder:
    cd caster-agents-v2

2. Install Dependencies

The framework uses Node.js packages. Install them with:

npm install

This downloads libraries like @neynar/nodejs-sdk, ethers, and openai. Wait until it finishes (it might take a minute or two).

3. Set Up Environment Variables

You need to provide your credentials securely using a .env file:

  1. Create a file named .env in the caster-agents-v2 folder:
    • On Windows: Use a text editor like Notepad and save it as .env (not .env.txt).
    • On macOS/Linux: Run touch .env in the terminal, then edit it with nano .env or another editor.
  2. Add the following lines to the .env file, replacing the placeholders with your actual credentials:
    FARCASTER_SIGNER_UUID=your_signer_uuid
    NEYNAR_API_KEY=your_neynar_api_key
    OPENAI_API_KEY=your_openai_api_key
    FID=your_fid
    CHANNEL=/yourchannel
    AGENT_PRIVATE_KEY=your_agent_private_key
    TOKEN_ADDRESS=your_token_contract_address
    CLANKER_API_KEY=your_clanker_api_key
    ``` ```
  3. Save and close the file.

Security Note: Keep your .env file private—never share it or upload it to GitHub.

4. Verify Your Setup

Ensure all files are in place:

  • index.ts: The main script.
  • clanker.ts: Clanker token deployment logic.
  • openai.ts: AI response generation.
  • package.json: Dependency list.
  • tsconfig.json: TypeScript configuration.

5. Run the Framework

Start the agent by running:

npm start

This compiles the TypeScript code and launches the agent. You’ll see logs like:

  • "Saindy is online, monitoring @casterapp mentions..."
  • Agent balances in ETH and $Saindy.

The agent will:

  • Check for mentions every 10 seconds.
  • Reply to mentions with AI responses and transfer 100 $Saindy (if the wallet has enough funds).
  • Deploy Clanker tokens if "deploy clanker" is mentioned.

6. Customize (Optional)

  • Change the Channel: Edit the CHANNEL variable in .env.
  • Adjust Token Amount: Modify ethers.parseEther('100') in sendTokens() in index.ts.
  • Edit AI Responses: Update the prompt in generateOpenAIResponse() in openai.ts.

7. Files Created

  • processed_ids.txt: Tracks processed mentions to avoid duplicates.
  • balances.json: Stores user token balances.

These are auto-generated when the agent runs.

Troubleshooting

  • "Command not found: git": Install Git from git-scm.com.
  • "npm not found": Install Node.js from nodejs.org.
  • Token Transfer Fails: Ensure the agent’s wallet has enough ETH (for gas) and $Saindy.
  • API Errors: Double-check your .env credentials.
  • Still Stuck?: Check the terminal for error messages or ask the community for help.

Need Help?

This is an open-source project! Join the community or check online for support.


Files You Need

Here are all the files required to run the Caster Agents Framework V2. Save them in a folder named caster-agents-v2.

1. index.ts

import fetch from 'node-fetch';
import { NeynarAPIClient } from '@neynar/nodejs-sdk';
import { config } from 'dotenv';
import fs from 'fs';
import { ethers } from 'ethers';
import { deployClankerToken } from './clanker';
import { generateOpenAIResponse } from './openai';

config();

const neynarClient = new NeynarAPIClient(process.env.NEYNAR_API_KEY as string);
const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const wallet = new ethers.Wallet(process.env.AGENT_PRIVATE_KEY as string, provider);
const tokenContract = new ethers.Contract(
  process.env.TOKEN_ADDRESS as string,
  [
    'function transfer(address to, uint256 amount) returns (bool)',
    'function balanceOf(address account) view returns (uint256)',
  ],
  wallet
);

const processedIdsFile = 'processed_ids.txt';
const balancesFile = 'balances.json';

interface Notification {
  type: string;
  cast?: {
    hash: string;
    text: string;
    author: {
      fid: number;
      username: string;
      verifications?: string[];
      custody_address: string;
    };
  };
}

function loadBalancesDB(): Record<string, number> {
  if (fs.existsSync(balancesFile)) {
    return JSON.parse(fs.readFileSync(balancesFile, 'utf8'));
  }
  return {};
}

function saveBalancesDB(balances: Record<string, number>) {
  fs.writeFileSync(balancesFile, JSON.stringify(balances, null, 2));
}

async function loadAgentBalances() {
  const ethBalance = await provider.getBalance(wallet.address);
  const tokenBalance = await tokenContract.balanceOf(wallet.address);
  console.log(`Agent ETH Balance: ${ethers.formatEther(ethBalance)} ETH`);
  console.log(`Agent $Saindy Balance: ${ethers.formatEther(tokenBalance)} $Saindy`);
}

async function getMentions(fid: number) {
  try {
    console.log('Fetching notifications...');
    const response = await neynarClient.fetchAllNotifications(fid, { type: 'mentions' });
    const notifications = response.notifications || [];
    const processedIds = fs.existsSync(processedIdsFile)
      ? fs.readFileSync(processedIdsFile, 'utf8').split('\n').filter(Boolean)
      : [];
    const mentions = notifications
      .filter(
        (n: Notification) =>
          n.type === 'mention' &&
          n.cast &&
          n.cast.hash &&
          !processedIds.includes(n.cast.hash)
      )
      .map((n: Notification) => ({
        cast: n.cast!,
        author: n.cast!.author,
        text: n.cast!.text,
        hash: n.cast!.hash,
      }));
    console.log(`Found ${mentions.length} new mentions`);
    return mentions;
  } catch (e) {
    console.error('Error fetching mentions:', e);
    return [];
  }
}

async function sendTokens(toAddress: string, fid: string): Promise<string | null> {
  try {
    const amount = ethers.parseEther('100');
    const balance = await tokenContract.balanceOf(wallet.address);
    if (balance < amount) {
      console.error(`Insufficient $Saindy balance: ${ethers.formatEther(balance)} $Saindy`);
      return null;
    }
    const feeData = await provider.getFeeData();
    const gasPrice = feeData.gasPrice;
    if (!gasPrice) return null;
    const tx = await tokenContract.transfer(toAddress, amount, {
      gasPrice: gasPrice * BigInt(2),
      gasLimit: 100000,
    });
    console.log(`Sending 100 $Saindy to ${toAddress} - TX: https://basescan.org/tx/${tx.hash}`);
    return tx.hash;
  } catch (e) {
    console.error('Token transfer failed:', e);
    return null;
  }
}

async function postToFarcaster(text: string, parentHash: string) {
  try {
    const cast = await neynarClient.publishCast(
      process.env.FARCASTER_SIGNER_UUID as string,
      text,
      { replyTo: parentHash }
    );
    console.log(`Posted reply: ${text}`);
    return cast;
  } catch (e) {
    console.error('Error posting to Farcaster:', e);
  }
}

async function generateResponse(mentionText: string, username: string, fid: string, txHash: string): Promise<string> {
  const balances = loadBalancesDB();
  const currentBalance = (balances[fid] || 0) + 100;
  balances[fid] = currentBalance;
  saveBalancesDB(balances);

  const prompt = `Generate a concise reply for @${username} who mentioned me on Farcaster with: "${mentionText}". Inform them I’ve sent 100 $Saindy (TX: https://basescan.org/tx/${txHash}) and their total balance is now ${currentBalance} $Saindy. Encourage further engagement subtly.`;
  return await generateOpenAIResponse(prompt);
}

async function processMention(mention: any) {
  const castText = mention.text.toLowerCase();
  const toAddress = mention.author.verifications?.[0] || mention.author.custody_address;
  const username = mention.author.username;
  const fid = mention.author.fid.toString();

  if (castText.includes('deploy clanker')) {
    console.log(`Deploying Clanker token for @${username}...`);
    const params = {
      name: 'Custom Clanker',
      symbol: 'CLK',
      image: 'https://example.com/clanker.png',
      requestorAddress: toAddress,
      requestKey: 'clanker_' + Date.now() + Math.random().toString(36).substring(2, 15),
      requestorFid: mention.author.fid,
    };
    const contractAddress = await deployClankerToken(params);
    if (contractAddress) {
      const txHash = await sendTokens(toAddress, fid);
      if (txHash) {
        const replyText = `Hey @${username}, your Clanker token is deployed at ${contractAddress}! I’ve sent you 100 $Saindy (TX: https://basescan.org/tx/${txHash}).`;
        await postToFarcaster(replyText, mention.hash);
      }
    }
  } else {
    const txHash = await sendTokens(toAddress, fid);
    if (txHash) {
      const replyText = await generateResponse(mention.text, username, fid, txHash);
      await postToFarcaster(replyText, mention.hash);
    }
  }
}

async function main() {
  const fid = parseInt(process.env.FID as string, 10);
  await loadAgentBalances();

  console.log('Saindy is online, monitoring @casterapp mentions...');

  while (true) {
    try {
      const mentions = await getMentions(fid);
      if (mentions.length > 0) {
        for (const mention of mentions) {
          await processMention(mention);
          fs.appendFileSync(processedIdsFile, `${mention.hash}\n`);
          await new Promise((resolve) => setTimeout(resolve, 5000));
        }
      }
      await new Promise((resolve) => setTimeout(resolve, 10000));
    } catch (e) {
      console.error('Main loop error:', e);
      await new Promise((resolve) => setTimeout(resolve, 60000));
    }
  }
}

main().catch((e) => console.error('Fatal error:', e));

2. clanker.ts

import fetch from 'node-fetch';

export interface DeployParams {
  name: string;
  symbol: string;
  image: string;
  requestorAddress: string;
  requestKey: string;
  requestorFid?: number;
  creatorRewardsPercentage?: number;
  tokenPair?: string;
  description?: string;
  socialMediaUrls?: string[];
  platform?: string;
  messageId?: string;
  vaultUnlockTimestamp?: number;
  vaultPercentage?: number;
  creatorRewardsAdmin?: string;
}

export async function deployClankerToken(params: DeployParams): Promise<string | null> {
  try {
    const response = await fetch('https://www.clanker.world/api/tokens/deploy', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': process.env.CLANKER_API_KEY as string,
      },
      body: JSON.stringify(params),
    });
    const data = await response.json();
    if (data.success && data.contract_address) {
      console.log(`Clanker token deployed at: ${data.contract_address}`);
      return data.contract_address;
    } else {
      console.error('Clanker deployment failed:', data.error || 'Unknown error');
      return null;
    }
  } catch (e) {
    console.error('Error deploying Clanker token:', e);
    return null;
  }
}

3. openai.ts

import fetch from 'node-fetch';

const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';

export async function generateOpenAIResponse(prompt: string): Promise<string> {
  const headers = {
    'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
    'Content-Type': 'application/json',
  };

  const body = JSON.stringify({
    model: 'gpt-4o-mini',
    messages: [
      {
        role: 'system',
        content: 'You are Saindy, a Farcaster AI agent. Generate concise, professional replies (1-2 sentences) that always include the user’s $Saindy token balance and a subtle encouragement to keep engaging. Avoid emojis and repetitive phrasing.',
      },
      {
        role: 'user',
        content: prompt,
      },
    ],
    max_tokens: 200,
    temperature: 0.5,
  });

  try {
    const response = await fetch(OPENAI_API_URL, {
      method: 'POST',
      headers,
      body,
    });
    const data = await response.json();
    if (data.choices && data.choices[0]) {
      return data.choices[0].message.content.trim();
    }
    throw new Error('No valid response from OpenAI');
  } catch (error) {
    console.error('OpenAI API error:', error);
    return 'Failed to generate a response due to an API issue.';
  }
}

4. package.json

{
  "name": "caster-agent-ts",
  "version": "1.0.0",
  "scripts": {
    "start": "ts-node index.ts"
  },
  "dependencies": {
    "@neynar/nodejs-sdk": "^1.71.0",
    "dotenv": "^16.4.7",
    "ethers": "^6.13.5",
    "node-fetch": "^2.7.0",
    "openai": "^4.0.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.0.0"
  }
}

5. tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./"
  },
  "include": ["*.ts"],
  "exclude": ["node_modules"]
}

How to Start from Scratch

  1. Create a Folder: Make a new folder called caster-agents-v2 on your computer.
  2. Save the Files: Copy each file above into the folder using a text editor (e.g., VS Code, Notepad).
  3. Follow the README: Start at "Step-by-Step Setup Guide" in the README.md above. It covers cloning (skip if you manually created the files), installing dependencies, setting up the .env, and running the framework.

That’s it! You now have everything to start the Caster Agents Framework V2. Follow the README steps, and your agent will be up and running.

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published