> Building the News Plugin for CypherNomad.eth

Evergreen
planted Jan 26, 2025tended Dec 27, 2025
#ai#web3#software-engineering#plugins

Introduction

After setting up the basic character and personality of CypherNomad.eth (detailed in my previous post), I focused on enhancing its capabilities with a news plugin. This addition allows the agent to search and share relevant news about whatever you ask.

Plugin Architecture

The news plugin is structured into three main components:

src/plugins/plugin-news/
β”œβ”€β”€ actions/
β”‚   β”œβ”€β”€ searchNewsAction.ts
β”‚   └── topHeadlinesAction.ts
β”œβ”€β”€ services/
β”‚   └── newsService.ts
β”œβ”€β”€ types.ts
β”œβ”€β”€ environment.ts
└── index.ts

Core Plugin Definition

// src/plugins/plugin-news/index.ts
export function createNewsPlugin() {
  return {
    name: "news",
    description: "News plugin providing access to news articles and headlines",
    services: [new NewsService()],
    actions: [searchNews, topHeadlines],
    evaluators: [],
    providers: [],
  } as const satisfies Plugin;
}

News Service Implementation

The NewsService handles all external API interactions and data processing:

// src/plugins/plugin-news/services/newsService.ts
export class NewsService {
  private readonly apiKey: string;
  private readonly baseUrl: string;

  constructor() {
    this.apiKey = process.env.NEWS_API_KEY;
    this.baseUrl = "https://newsapi.org/v2";
  }

  async searchNews(
    query: string,
    options?: SearchOptions
  ): Promise<NewsResponse> {
    const searchParams = new URLSearchParams({
      q: query,
      apiKey: this.apiKey,
      language: options?.language || "en",
      sortBy: options?.sortBy || "relevancy",
      pageSize: String(options?.pageSize || 10),
    });

    const response = await fetch(`${this.baseUrl}/everything?${searchParams}`);
    return this.processResponse(response);
  }

  async getTopHeadlines(category?: string): Promise<NewsResponse> {
    const searchParams = new URLSearchParams({
      apiKey: this.apiKey,
      category: category || "technology",
      language: "en",
    });

    const response = await fetch(
      `${this.baseUrl}/top-headlines?${searchParams}`
    );
    return this.processResponse(response);
  }

  private async processResponse(response: Response): Promise<NewsResponse> {
    if (!response.ok) {
      throw new Error(`News API Error: ${response.statusText}`);
    }
    return response.json();
  }
}

Action Implementations

Search News Action

// src/plugins/plugin-news/actions/searchNewsAction.ts
export const searchNews: Action = {
  name: "searchNews",
  description: "Search for news articles based on a query",
  parameters: {
    type: "object",
    properties: {
      query: {
        type: "string",
        description: "The search query for news articles",
      },
      options: {
        type: "object",
        properties: {
          language: { type: "string" },
          sortBy: { type: "string" },
          pageSize: { type: "number" },
        },
        required: [],
      },
    },
    required: ["query"],
  },

  async execute({ query, options }, context) {
    const newsService = context.services.get(NewsService);
    const articles = await newsService.searchNews(query, options);

    return formatNewsResponse(articles);
  },
};

Top Headlines Action

// src/plugins/plugin-news/actions/topHeadlinesAction.ts
export const topHeadlines: Action = {
  name: "topHeadlines",
  description: "Get top headlines, optionally filtered by category",
  parameters: {
    type: "object",
    properties: {
      category: {
        type: "string",
        enum: ["technology", "business", "science"],
        description: "Category to filter headlines",
      },
    },
    required: [],
  },

  async execute({ category }, context) {
    const newsService = context.services.get(NewsService);
    const headlines = await newsService.getTopHeadlines(category);

    return formatNewsResponse(headlines);
  },
};

Response Formatting

The plugin includes utilities to format news responses in a consistent way:

// src/plugins/plugin-news/utils/formatters.ts
export function formatNewsResponse(
  response: NewsResponse
): FormattedNewsResponse {
  return {
    articles: response.articles.map((article) => ({
      title: article.title,
      description: article.description,
      url: article.url,
      source: article.source.name,
      publishedAt: new Date(article.publishedAt).toISOString(),
      formattedDate: formatDate(article.publishedAt),
    })),
    totalResults: response.totalResults,
  };
}

function formatDate(dateString: string): string {
  const date = new Date(dateString);
  return date.toLocaleDateString("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
  });
}

Type Definitions

Strong typing ensures consistency and reliability:

// src/plugins/plugin-news/types.ts
export interface NewsResponse {
  status: string;
  totalResults: number;
  articles: Article[];
}

export interface Article {
  source: {
    id: string | null;
    name: string;
  };
  author: string | null;
  title: string;
  description: string;
  url: string;
  urlToImage: string | null;
  publishedAt: string;
  content: string;
}

export interface SearchOptions {
  language?: string;
  sortBy?: "relevancy" | "popularity" | "publishedAt";
  pageSize?: number;
}

export interface FormattedNewsResponse {
  articles: FormattedArticle[];
  totalResults: number;
}

export interface FormattedArticle {
  title: string;
  description: string;
  url: string;
  source: string;
  publishedAt: string;
  formattedDate: string;
}

Environment Configuration

// src/plugins/plugin-news/environment.ts
export const requiredEnvVars = {
  NEWS_API_KEY: process.env.NEWS_API_KEY,
};

export function validateEnvironment(): void {
  const missing = Object.entries(requiredEnvVars)
    .filter(([, value]) => !value)
    .map(([key]) => key);

  if (missing.length > 0) {
    throw new Error(
      `Missing required environment variables: ${missing.join(", ")}`
    );
  }
}

Usage Examples

Here's how CypherNomad.eth uses these actions:

// Example: Searching for specific news
const cryptoNews = await context.actions.searchNews({
  query: "ethereum AND (cryptography OR blockchain)",
  options: {
    sortBy: "relevancy",
    pageSize: 5,
  },
});

// Example: Getting top tech headlines
const headlines = await context.actions.topHeadlines({
  category: "technology",
});

Future Enhancements

Planned improvements for the news plugin include:

  1. Advanced Filtering

    • Sentiment analysis
    • Source credibility scoring
    • Content categorization
  2. Custom Formatting

    • Markdown support
    • Social media optimized formats
    • Rich media embedding
  3. Caching Layer

    • Redis integration
    • Rate limiting
    • Response caching

Resources and Links


This plugin is part of the ongoing development of CypherNomad.eth. Contributions and feedback are welcome!