Last30Days is a composable agent skill that aggregates search results from Reddit, X, YouTube, Hacker News, Polymarket, TikTok, and GitHub. It handles authentication state across seven different API providers, executes searches in parallel, normalizes engagement signals into a unified score, and returns synthesized summaries to any agent runtime that supports the Agent Skills specification.
The project hit #6 on GitHub Trending for Python with 170 stars. It demonstrates the emerging pattern of agent skills as discrete, installable units that bridge walled-garden APIs no single LLM provider can access natively.
The Agent Skill Abstraction
Agent skills are not plugins or extensions. They are standalone executables that expose a command interface, manage their own dependencies, and run in isolated processes. The runtime (Claude Code, Cursor, Copilot, Gemini CLI) invokes the skill via a subprocess call, passes arguments as JSON or CLI flags, and receives structured output.
Last30Days ships as a Python package with a CLI entrypoint. Installation via npx skills add mvanhorn/last30days-skill -g registers the skill globally. The agent runtime reads SKILL.md for command signatures and invokes /last30days <query> when the user or agent decides to search.
The skill does not run inside the agent’s process. It does not share memory or state. It communicates over stdin/stdout with JSON payloads. This boundary isolates credential management, rate limiting, and API client logic from the agent runtime.
Authentication State Management
Last30Days supports seven platforms with three authentication patterns:
| Platform | Auth Method | Credential Storage | Setup Required |
|---|---|---|---|
| Public API | None | No | |
| HN | Public API | None | No |
| Polymarket | Public API | None | No |
| GitHub | Public API | None | No |
| X | Bearer token | Local config file | Yes |
| YouTube | API key | Local config file | Yes |
| TikTok | Session cookie | Local config file | Yes |
On first run, the skill checks for a local .last30days/config.json file. If missing, it launches a setup wizard that prompts for API keys and tokens. The wizard writes credentials to disk in the user’s home directory, outside the agent runtime’s visibility.
The skill never passes credentials to the agent. The agent runtime never sees API keys. The skill process reads credentials from disk, constructs authenticated HTTP clients, and executes requests directly.
This pattern avoids the credential leakage risk of passing secrets through agent prompts or environment variables visible to the LLM.
Parallel Execution Model
Last30Days fans out search requests in parallel using Python’s asyncio and aiohttp. The main execution flow:
- Parse the user query from CLI arguments
- Spawn async tasks for each enabled platform
- Each task constructs an authenticated HTTP client
- Each task executes a platform-specific search (Reddit uses
/search.json, X uses/2/tweets/search/recent, YouTube uses/youtube/v3/search) - Each task returns a list of results with raw engagement metrics
- The aggregator waits for all tasks to complete or timeout
- Failed tasks return empty lists without blocking other platforms
Rate limiting is handled per-platform. Reddit and HN have no rate limits for public endpoints. X enforces 180 requests per 15-minute window. YouTube enforces 10,000 quota units per day. The skill does not implement retry logic or backoff. If a platform fails, it logs the error and continues.
Timeouts are set at 10 seconds per platform. If a platform does not respond within 10 seconds, the task cancels and returns an empty result set.
Cross-Platform Data Normalization
Each platform returns different engagement signals:
- Reddit: upvotes, comments, awards
- X: likes, retweets, replies
- YouTube: views, likes, comments
- HN: points, comments
- Polymarket: volume, probability, liquidity
- TikTok: likes, shares, comments
- GitHub: stars, forks, watchers
The skill normalizes these into a unified score using a weighted formula:
def calculate_score(result):
platform = result['platform']
if platform == 'reddit':
return result['upvotes'] * 1.0 + result['comments'] * 0.5
elif platform == 'x':
return result['likes'] * 1.0 + result['retweets'] * 2.0
elif platform == 'youtube':
return (result['views'] / 1000) * 0.1 + result['likes'] * 1.0
elif platform == 'hn':
return result['points'] * 1.0 + result['comments'] * 0.3
elif platform == 'polymarket':
return result['volume'] * 0.01 + result['liquidity'] * 0.005
elif platform == 'tiktok':
return result['likes'] * 1.0 + result['shares'] * 1.5
elif platform == 'github':
return result['stars'] * 1.0 + result['forks'] * 0.5
return 0
The formula is hardcoded. It does not adapt to query type or user preferences. Polymarket volume is weighted lower because dollar amounts are larger than engagement counts. YouTube views are divided by 1000 to prevent view count from dominating the score.
After scoring, the skill sorts results by score descending and returns the top 50 items to the agent runtime as a JSON array.
Synthesis and Output Format
The skill returns structured JSON with three sections:
- Top Results: The 50 highest-scored items with title, URL, platform, score, and snippet
- Platform Summary: Counts of results per platform
- Metadata: Query timestamp, total results, execution time
The agent runtime receives this JSON and decides how to present it. Claude Code renders it as a formatted list. Cursor inlines it into the chat. The skill does not control presentation.
The agent can also request a synthesized summary by passing a --synthesize flag. In this mode, the skill sends the top 20 results to an LLM with a prompt:
Synthesize these search results into a 3-paragraph summary.
Focus on consensus, outliers, and high-confidence signals.
Cite sources inline with [platform:title] format.
The LLM response is appended to the JSON output under a synthesis key. This requires an API key in the config file. If missing, synthesis is skipped.
Failure Modes and Observability
The skill logs to stderr. The agent runtime does not capture these logs by default. To debug, run the skill directly:
last30days "AI agent frameworks" --debug
Common failure modes:
- API key invalid: The skill returns an error JSON with
{"error": "X authentication failed"}. The agent sees the error and can prompt the user to reconfigure. - Rate limit exceeded: The skill returns partial results from platforms that succeeded. No retry logic.
- Network timeout: The skill returns partial results. Timed-out platforms are omitted from the output.
- Malformed query: The skill returns an error JSON with
{"error": "Query must be non-empty"}.
The skill does not emit structured telemetry. It does not integrate with OpenTelemetry or Langfuse. Observability is limited to stderr logs and error JSON.
Deployment Shape
Last30Days is not a service. It is a CLI tool installed per-user. The agent runtime invokes it as a subprocess. There is no server, no container, no orchestration layer.
For multi-user deployments (e.g., a team using the same agent runtime), each user must install the skill and configure their own API keys. There is no shared credential store.
For hosted agent runtimes (e.g., claude.ai web), the skill runs in a sandboxed environment with ephemeral storage. Credentials do not persist across sessions. The user must reconfigure on every session start.
The skill does not support Docker or Kubernetes deployment. It is designed for local execution by a single user.
Security Boundaries
The skill reads credentials from ~/.last30days/config.json. This file is world-readable by default on Unix systems. The skill does not set restrictive permissions. A local attacker with shell access can read API keys.
The skill does not encrypt credentials at rest. It does not use OS keychains or secret managers. It assumes the user’s home directory is trusted.
The skill does not validate SSL certificates for API requests. It uses the default aiohttp behavior, which validates certificates but does not pin them. A man-in-the-middle attacker on the network can intercept API traffic.
The skill does not sanitize user queries before sending them to platform APIs. A malicious query could trigger unintended API behavior (e.g., SQL injection if a platform has a vulnerable search endpoint). The skill assumes platform APIs are safe.
Technical Verdict
Use Last30Days if:
- You’re building a personal research agent and can manage API keys in
~/.last30days/config.jsonon your local machine - You need to aggregate engagement signals across 7+ platforms without building custom API clients for each
- Your agent runtime supports subprocess-based skills (Claude Code, Cursor, Copilot, Gemini CLI)
- You’re comfortable with partial results when individual platforms fail or hit rate limits
- You trust your local filesystem for credential storage and don’t need encryption at rest
Avoid Last30Days if:
- You need multi-user credential isolation (e.g., shared team agent runtime where users should not see each other’s API keys)
- You require production observability with structured telemetry, retry logic, circuit breakers, or SLA guarantees
- Your platform APIs are behind corporate proxies or require certificate pinning
- You need hosted deployment in sandboxed environments where credentials must persist across sessions
- You’re aggregating sensitive data and need encrypted credential storage or integration with secret managers
The agent skill abstraction is the valuable pattern here. It shows how to build composable units that bridge multiple proprietary APIs without coupling to a specific LLM provider or agent framework. The subprocess boundary keeps credentials isolated from the agent runtime, and the JSON interface makes the skill portable across 50+ agent hosts.
The pattern scales to other multi-platform integrations: aggregating Slack, Discord, and Teams messages; combining Stripe, PayPal, and Coinbase transaction data; or searching across internal wikis, Notion, and Google Drive. The key is the isolated process model and the standardized command interface.
The scoring formula is functional but naive. A production version would need query-aware weighting (prioritize Polymarket for prediction queries, prioritize YouTube for tutorial queries) and user-configurable weights. The hardcoded multipliers work for general search but break down when query intent varies.