Skip to content
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
37 changes: 35 additions & 2 deletions typescript/agentkit/src/action-providers/farcaster/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,39 @@ farcaster/

## Actions

- `account_details`: Get the details of the Farcaster account
- `account_details`: Get the details of the agent's Farcaster account

- Accepts the FID of the Farcaster account to get details for
- Returns the agent's account information

- `get_user_details`: Get the details of any Farcaster user

- Look up users by username or FID
- Useful for getting information about other users before interacting

- `post_cast`: Create a new Farcaster post

- Supports text content up to 280 characters
- Supports up to 2 embedded URLs via the optional `embeds` parameter

- `reply_to_cast`: Reply to an existing cast

- Takes the parent cast hash and reply text
- Supports up to 2 embedded URLs
- Enables conversational interactions on Farcaster

- `get_feed`: Get a user's casts/feed

- Retrieve casts from any user (defaults to agent's own casts)
- Configurable limit (1-100 casts)
- Option to include or exclude replies
- Useful for getting context on previous conversations

- `get_mentions`: Get casts that mention the agent

- Retrieve notifications where the agent was mentioned
- Enables the agent to respond to users who have tagged them
- Configurable limit (1-100 mentions)

## Adding New Actions

To add new Farcaster actions:
Expand All @@ -36,9 +60,18 @@ To add new Farcaster actions:

The Farcaster provider supports all EVM-compatible networks.

## Environment Variables

| Variable | Description |
|----------|-------------|
| `NEYNAR_API_KEY` | Your Neynar API key |
| `NEYNAR_MANAGER_SIGNER` | The managed signer UUID for posting |
| `AGENT_FID` | The FID of your agent's Farcaster account |

## Notes

- Requires a Neynar API key. Visit the [Neynar Dashboard](https://dev.neynar.com/) to get your key.
- Embeds allow you to attach URLs to casts that will render as rich previews in the Farcaster client
- The `get_mentions` action is useful for building conversational agents that respond to user interactions

For more information on the **Farcaster Protocol**, visit [Farcaster Documentation](https://docs.farcaster.xyz/).
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { FarcasterActionProvider } from "./farcasterActionProvider";
import { FarcasterAccountDetailsSchema, FarcasterPostCastSchema } from "./schemas";
import {
FarcasterAccountDetailsSchema,
FarcasterPostCastSchema,
FarcasterGetUserDetailsSchema,
FarcasterReplyCastSchema,
FarcasterGetFeedSchema,
FarcasterGetMentionsSchema,
} from "./schemas";

// Mock fetch globally
const mockFetch = jest.fn();
Expand Down Expand Up @@ -248,4 +255,250 @@ describe("Farcaster Action Provider", () => {
expect(actionProvider.supportsNetwork({ protocolFamily: "solana" })).toBe(false);
});
});

describe("getUserDetails", () => {
const mockUserResponse = {
users: [
{
object: "user",
fid: 123,
username: "testuser",
display_name: "Test User",
},
],
};

it("should successfully retrieve user details by FID", async () => {
mockFetch.mockResolvedValueOnce({
json: () => Promise.resolve(mockUserResponse),
});

const result = await actionProvider.getUserDetails({ fid: 123 });

expect(mockFetch).toHaveBeenCalledWith(
"https://api.neynar.com/v2/farcaster/user/bulk?fids=123",
expect.objectContaining({
method: "GET",
}),
);
expect(result).toContain("Successfully retrieved Farcaster user details");
});

it("should successfully retrieve user details by username", async () => {
mockFetch.mockResolvedValueOnce({
json: () => Promise.resolve({ user: mockUserResponse.users[0] }),
});

const result = await actionProvider.getUserDetails({ username: "testuser" });

expect(mockFetch).toHaveBeenCalledWith(
"https://api.neynar.com/v2/farcaster/user/by_username?username=testuser",
expect.objectContaining({
method: "GET",
}),
);
expect(result).toContain("Successfully retrieved Farcaster user details");
});

it("should handle user not found", async () => {
mockFetch.mockResolvedValueOnce({
json: () => Promise.resolve({ users: [] }),
});

const result = await actionProvider.getUserDetails({ fid: 99999 });

expect(result).toContain("User not found");
});
});

describe("replyToCast", () => {
const mockReplyResponse = {
cast: {
hash: "0x456",
text: "This is a reply",
},
};

it("should successfully reply to a cast", async () => {
mockFetch.mockResolvedValueOnce({
json: () => Promise.resolve(mockReplyResponse),
});

const args = {
parentHash: "0x123",
replyText: "This is a reply",
};

const result = await actionProvider.replyToCast(args);

expect(mockFetch).toHaveBeenCalledWith("https://api.neynar.com/v2/farcaster/cast", {
method: "POST",
headers: {
api_key: mockConfig.neynarApiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
signer_uuid: mockConfig.signerUuid,
text: args.replyText,
parent: args.parentHash,
embeds: undefined,
}),
});
expect(result).toContain("Successfully posted reply to Farcaster");
});

it("should handle errors when replying", async () => {
const error = new Error("Failed to reply");
mockFetch.mockRejectedValueOnce(error);

const args = {
parentHash: "0x123",
replyText: "This is a reply",
};

const result = await actionProvider.replyToCast(args);

expect(result).toBe(`Error posting reply to Farcaster:\n${error}`);
});
});

describe("getFeed", () => {
const mockFeedResponse = {
casts: [
{ hash: "0x1", text: "Cast 1" },
{ hash: "0x2", text: "Cast 2" },
],
};

it("should successfully retrieve feed for agent", async () => {
mockFetch.mockResolvedValueOnce({
json: () => Promise.resolve(mockFeedResponse),
});

const result = await actionProvider.getFeed({ limit: 10, includeReplies: false });

expect(mockFetch).toHaveBeenCalledWith(
`https://api.neynar.com/v2/farcaster/feed/user/casts?fid=${mockConfig.agentFid}&limit=10&include_replies=false`,
expect.objectContaining({
method: "GET",
}),
);
expect(result).toContain("Successfully retrieved Farcaster feed");
expect(result).toContain("2 casts");
});

it("should retrieve feed for specified FID", async () => {
mockFetch.mockResolvedValueOnce({
json: () => Promise.resolve(mockFeedResponse),
});

const result = await actionProvider.getFeed({ fid: 456, limit: 25, includeReplies: true });

expect(mockFetch).toHaveBeenCalledWith(
"https://api.neynar.com/v2/farcaster/feed/user/casts?fid=456&limit=25&include_replies=true",
expect.objectContaining({
method: "GET",
}),
);
expect(result).toContain("Successfully retrieved Farcaster feed");
});
});

describe("getMentions", () => {
const mockMentionsResponse = {
notifications: [
{ cast: { hash: "0x1", text: "Hey @agent" } },
{ cast: { hash: "0x2", text: "Hello @agent" } },
],
};

it("should successfully retrieve mentions", async () => {
mockFetch.mockResolvedValueOnce({
json: () => Promise.resolve(mockMentionsResponse),
});

const result = await actionProvider.getMentions({ limit: 10 });

expect(mockFetch).toHaveBeenCalledWith(
`https://api.neynar.com/v2/farcaster/notifications?fid=${mockConfig.agentFid}&type=mentions&limit=10`,
expect.objectContaining({
method: "GET",
}),
);
expect(result).toContain("Successfully retrieved Farcaster mentions");
expect(result).toContain("2 mentions");
});

it("should handle errors when retrieving mentions", async () => {
const error = new Error("Failed to get mentions");
mockFetch.mockRejectedValueOnce(error);

const result = await actionProvider.getMentions({ limit: 25 });

expect(result).toBe(`Error retrieving Farcaster mentions:\n${error}`);
});
});
});

describe("New Farcaster Schema Tests", () => {
describe("GetUserDetails Schema", () => {
it("should accept username", () => {
const result = FarcasterGetUserDetailsSchema.safeParse({ username: "testuser" });
expect(result.success).toBe(true);
});

it("should accept fid", () => {
const result = FarcasterGetUserDetailsSchema.safeParse({ fid: 123 });
expect(result.success).toBe(true);
});

it("should reject when neither username nor fid provided", () => {
const result = FarcasterGetUserDetailsSchema.safeParse({});
expect(result.success).toBe(false);
});
});

describe("ReplyCast Schema", () => {
it("should accept valid reply", () => {
const result = FarcasterReplyCastSchema.safeParse({
parentHash: "0x123",
replyText: "This is a reply",
});
expect(result.success).toBe(true);
});

it("should reject reply over 280 characters", () => {
const result = FarcasterReplyCastSchema.safeParse({
parentHash: "0x123",
replyText: "a".repeat(281),
});
expect(result.success).toBe(false);
});
});

describe("GetFeed Schema", () => {
it("should accept valid feed request", () => {
const result = FarcasterGetFeedSchema.safeParse({
limit: 25,
includeReplies: true,
});
expect(result.success).toBe(true);
});

it("should reject limit over 100", () => {
const result = FarcasterGetFeedSchema.safeParse({
limit: 101,
});
expect(result.success).toBe(false);
});
});

describe("GetMentions Schema", () => {
it("should accept valid mentions request", () => {
const result = FarcasterGetMentionsSchema.safeParse({
limit: 25,
});
expect(result.success).toBe(true);
});
});
});
Loading