Skip to content

Commit d329efd

Browse files
RipperMercsclaude
andcommitted
feat(news-search): premium full-text article search across worker, SDKs, MCP
New /api/premium/news/search (Tier 1, 1 credit) plus matching SDK and MCP support so AI agents can ask "what happened with X in March?" in one paid call instead of paginating the free /news endpoint themselves. Worker (worker/src/news-search.ts): - Tokenization strips stop words and tokens shorter than 2 chars - Relevance score blends title hits (weight 3), snippet hits (weight 1), and a recency boost in [0, 0.3] capped at score 1.0 - Filters: q (free text), from / to (YYYY-MM-DD UTC), provider (substring match against source name and sourceDomain), category (substring match), limit (1-100, default 25) - Browse mode: omit q to get the latest filtered articles in publishedAt desc with relevance 1 - Validation rejects malformed dates and from > to with 400 SDKs: Python 1.7.0 tf.news_search(q=, from_date=, to_date=, provider=, category=, limit=) TypeScript 1.6.0 tf.newsSearch({ q, from, to, provider, category, limit }) TS exports NewsSearchResponse + NewsSearchResultItem MCP (1.2.0): news_search tool with formatted text output (rank, title, source, url, date, relevance, matched terms, snippet) Tests: 15 new vitest cases for tokenization, scoring, filters, browse mode, validation, and empty corpus. Total worker tests: 95. Also restored proper X_ACCESS_TOKEN / GITHUB_TOKEN field names in worker/src/payments.test.ts (an over-aggressive rename had renamed them in a prior commit; runtime tests passed but tsc was unhappy). Updated /api/meta, public/llms.txt, /developers/agent-payments, CLAUDE.md, and all three READMEs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3be5e3e commit d329efd

17 files changed

Lines changed: 789 additions & 10 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ tensorfeed/
4343
watches.test.ts Vitest coverage for predicate edge transitions, SSRF guard, dispatch end-to-end
4444
agents-enriched.ts Premium enriched agents directory: joins catalog with status, news, activity, pricing; trending score; sort/filter
4545
agents-enriched.test.ts Vitest coverage for enrichment join logic, filters, sort, scoring
46+
news-search.ts Premium news search: tokenization, relevance scoring (title weight 3, snippet 1, recency boost), date/provider/category filters
47+
news-search.test.ts Vitest coverage for query mode, filters, browse mode, validation
4648
podcasts.ts Podcast feed polling
4749
trending.ts Trending GitHub repos
4850
twitter.ts X/Twitter auto-posting
@@ -205,6 +207,7 @@ All mounted under `https://tensorfeed.ai/api/*` via the Worker.
205207
- `/api/premium/watches` (GET): List watches owned by the bearer token. Free.
206208
- `/api/premium/watches/{id}` (GET|DELETE): Read or remove an owned watch. Free.
207209
- `/api/premium/agents/directory?category=&status=&open_source=&capability=&sort=&limit=`: Tier 1, 1 credit. Enriched agents catalog joined with live status, recent news (count + top 3), agent traffic, flagship pricing, and a derived trending_score. Sort options: trending, alphabetical, status, price_low, price_high, news_count.
210+
- `/api/premium/news/search?q=&from=&to=&provider=&category=&limit=`: Tier 1, 1 credit. Full-text search over the article corpus with relevance scoring (term hits weighted 3 in title, 1 in snippet, plus recency boost) and date/provider/category filters. Default limit 25, max 100.
208211

209212
**Admin (auth-gated via `?key=ENVIRONMENT`):**
210213
- `/api/admin/usage?date=YYYY-MM-DD`: Daily revenue + usage rollup

mcp-server/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ These tools require a `TENSORFEED_TOKEN` env var. Buy credits at [tensorfeed.ai/
2626
| `status_uptime` | 1 credit | Daily uptime % for one provider with incident-day list |
2727
| `history_compare` | 1 credit | Diff two daily snapshots: added, removed, changed entries |
2828
| `premium_agents_directory` | 1 credit | Enriched agents catalog with live status, news, traffic, trending_score |
29+
| `news_search` | 1 credit | Full-text news search with date/provider/category filters and relevance scoring |
2930
| `list_watches` | Free | List active webhook watches owned by the token |
3031
| `create_price_watch` | 1 credit | Register a webhook watch on a model price change |
3132
| `create_status_watch` | 1 credit | Register a webhook watch on a service status transition |

mcp-server/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@tensorfeed/mcp-server",
3-
"version": "1.1.0",
4-
"description": "MCP server for TensorFeed.ai - AI news, status, model data, and premium endpoints (routing, history series, webhook watches, enriched directory) for AI agents",
3+
"version": "1.2.0",
4+
"description": "MCP server for TensorFeed.ai - AI news, status, model data, and premium endpoints (routing, history series, news search, webhook watches, enriched directory) for AI agents",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
77
"type": "module",

mcp-server/src/index.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
55
import { z } from 'zod';
66

77
const API_BASE = 'https://tensorfeed.ai/api';
8-
const SDK_VERSION = '1.1.0';
8+
const SDK_VERSION = '1.2.0';
99

1010
// ── API helpers ─────────────────────────────────────────────────────
1111

@@ -550,6 +550,54 @@ server.tool(
550550
},
551551
);
552552

553+
// ── Tool: news_search (1 credit) ────────────────────────────────────
554+
555+
server.tool(
556+
'news_search',
557+
'Full-text search over the TensorFeed news article corpus with optional date range, provider, and category filters. Relevance scoring with recency boost. Costs 1 credit.',
558+
{
559+
q: z.string().optional().describe('Free-text query, e.g. "claude opus pricing". Omit to browse latest filtered articles.'),
560+
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (inclusive)'),
561+
to: z.string().optional().describe('End date YYYY-MM-DD UTC (inclusive end-of-day)'),
562+
provider: z.string().optional().describe('Substring match against source name and domain (e.g. "anthropic", "openai", "techcrunch")'),
563+
category: z.string().optional().describe('Substring match against article categories'),
564+
limit: z.number().min(1).max(100).optional().describe('Max results (default 25, max 100)'),
565+
},
566+
async ({ q, from, to, provider, category, limit }) => {
567+
const params = new URLSearchParams();
568+
if (q) params.set('q', q);
569+
if (from) params.set('from', from);
570+
if (to) params.set('to', to);
571+
if (provider) params.set('provider', provider);
572+
if (category) params.set('category', category);
573+
if (typeof limit === 'number') params.set('limit', String(limit));
574+
const data = (await fetchJSON(`/premium/news/search?${params}`, { auth: true })) as {
575+
query: string | null;
576+
matched: number;
577+
returned: number;
578+
results: { title: string; url: string; source: string; published_at: string; relevance: number; matched_terms: string[]; snippet: string }[];
579+
billing?: { credits_remaining?: number };
580+
};
581+
if (data.results.length === 0) {
582+
return { content: [{ type: 'text' as const, text: `No articles matched. (matched: ${data.matched})` }] };
583+
}
584+
const list = data.results
585+
.map(
586+
(r, i) =>
587+
`${i + 1}. ${r.title} (${r.source})\n ${r.url}\n ${r.published_at} | relevance ${r.relevance}${r.matched_terms.length ? ` | terms: ${r.matched_terms.join(', ')}` : ''}\n ${r.snippet}`,
588+
)
589+
.join('\n\n');
590+
return {
591+
content: [
592+
{
593+
type: 'text' as const,
594+
text: `${data.returned} of ${data.matched} matches${data.query ? ` for "${data.query}"` : ''}:\n\n${list}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
595+
},
596+
],
597+
};
598+
},
599+
);
600+
553601
// ── Tool: list_watches ──────────────────────────────────────────────
554602

555603
server.tool(

public/llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ TensorFeed accepts USDC on Base as the sole payment method for premium endpoints
5656
- [Premium History Compare](https://tensorfeed.ai/api/premium/history/compare): Tier 1, 1 credit per call. Diff between two daily snapshots: added, removed, and changed entries with deltas. Params: `?from=YYYY-MM-DD&to=YYYY-MM-DD&type=pricing|benchmarks`.
5757
- [Premium Watches](https://tensorfeed.ai/api/premium/watches): Tier 1, 1 credit per registration. Webhook alerts for price changes (`{ type: "price", model, field: "inputPrice"|"outputPrice"|"blended", op: "lt"|"gt"|"changes", threshold? }`) or service status transitions (`{ type: "status", provider, op: "becomes"|"changes", value? }`). POST `{ spec, callback_url, secret? }` to register, GET to list, GET/DELETE on `/api/premium/watches/{id}` for individual control. Each watch lives 90 days, fires up to 100 times by default, delivers a signed POST to the callback URL with `X-TensorFeed-Signature: sha256=<hex>` and an `X-TensorFeed-Watch-Id` header. Listing and per-watch read/delete require the bearer token but cost no credits.
5858
- [Premium Agents Directory](https://tensorfeed.ai/api/premium/agents/directory): Tier 1, 1 credit per call. The agents catalog joined with live status, recent news count + top 3 articles, agent traffic stats, flagship pricing, and a derived `trending_score` (0-100). Server-side filter (`?category=&status=&open_source=&capability=`) and sort (`?sort=trending|alphabetical|status|price_low|price_high|news_count`). Default `limit=50`, max 100.
59+
- [Premium News Search](https://tensorfeed.ai/api/premium/news/search): Tier 1, 1 credit per call. Full-text search over the news corpus with relevance scoring (term hits in title weighted 3, snippet 1, plus recency boost). Filters: `?q=&from=YYYY-MM-DD&to=YYYY-MM-DD&provider=&category=&limit=`. Omit `q` to browse latest filtered articles in publishedAt desc. Stop words and short tokens are stripped. Each result includes title, url, source, snippet, published_at, relevance, and matched_terms. Default `limit=25`, max 100.
5960

6061
**Recommended flow:** POST `/api/payment/buy-credits`, send USDC, POST `/api/payment/confirm`, then use the returned token for all premium calls. **Fallback (x402):** call any `/api/premium/*` endpoint without auth to receive a 402 response with payment instructions; send USDC and retry with `X-Payment-Tx: <txHash>` header.
6162

sdk/javascript/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ try {
199199
| `tf.getWatch(id)` | Free | Read one watch including fire_count and last_fired_at |
200200
| `tf.deleteWatch(id)` | Free | Remove an owned watch |
201201
| `tf.premiumAgentsDirectory({ category?, status?, sort?, limit?, ... })` | 1 credit | Enriched directory: status, news, traffic, pricing, trending_score per agent |
202+
| `tf.newsSearch({ q?, from?, to?, provider?, category?, limit? })` | 1 credit | Full-text news search with date/provider filters, relevance scoring, recency boost |
202203

203204
## Wallet & Trust
204205

sdk/javascript/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tensorfeed",
3-
"version": "1.5.0",
3+
"version": "1.6.0",
44
"description": "JavaScript/TypeScript SDK for the TensorFeed.ai API: AI news, status, model pricing, benchmarks, premium history series, webhook watches, and agent-payable premium routing (USDC on Base)",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

sdk/javascript/src/index.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*/
1111

1212
const DEFAULT_BASE_URL = 'https://tensorfeed.ai/api';
13-
const DEFAULT_USER_AGENT = 'TensorFeed-SDK-JS/1.5';
13+
const DEFAULT_USER_AGENT = 'TensorFeed-SDK-JS/1.6';
1414

1515
// ── Error types ─────────────────────────────────────────────────────
1616

@@ -421,6 +421,29 @@ export type AgentsDirectorySort =
421421
| 'price_high'
422422
| 'news_count';
423423

424+
export interface NewsSearchResultItem {
425+
title: string;
426+
url: string;
427+
source: string;
428+
source_domain: string;
429+
snippet: string;
430+
categories: string[];
431+
published_at: string;
432+
relevance: number;
433+
matched_terms: string[];
434+
}
435+
436+
export interface NewsSearchResponse {
437+
ok: boolean;
438+
query: string | null;
439+
filters: { from?: string; to?: string; provider?: string; category?: string };
440+
total_corpus: number;
441+
matched: number;
442+
returned: number;
443+
results: NewsSearchResultItem[];
444+
billing?: { credits_charged: number; credits_remaining?: number };
445+
}
446+
424447
export interface PremiumAgentsDirectoryResponse {
425448
ok: boolean;
426449
source: 'tensorfeed.ai';
@@ -955,6 +978,40 @@ export class TensorFeed {
955978
requireToken: true,
956979
});
957980
}
981+
982+
// ── Paid: news search (1 credit per call) ──────────────────────
983+
984+
/**
985+
* Full-text search over the TensorFeed news article corpus with date
986+
* range, provider, and category filters. Relevance scoring blends term
987+
* hits in title (weight 3) and snippet (weight 1) plus a recency boost.
988+
* Costs 1 credit. Stop words and tokens shorter than 2 chars are
989+
* stripped from the query.
990+
*
991+
* @throws Error if no token is set
992+
* @throws PaymentRequired if the token has insufficient credits
993+
*/
994+
async newsSearch(options?: {
995+
q?: string;
996+
from?: string;
997+
to?: string;
998+
provider?: string;
999+
category?: string;
1000+
limit?: number;
1001+
}): Promise<NewsSearchResponse> {
1002+
this.requireToken('newsSearch');
1003+
return this.request<NewsSearchResponse>('GET', '/premium/news/search', {
1004+
params: {
1005+
q: options?.q,
1006+
from: options?.from,
1007+
to: options?.to,
1008+
provider: options?.provider,
1009+
category: options?.category,
1010+
limit: options?.limit,
1011+
},
1012+
requireToken: true,
1013+
});
1014+
}
9581015
}
9591016

9601017
export default TensorFeed;

sdk/python/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ except TensorFeedError as e:
221221
| `tf.get_watch(watch_id)` | Free | Read one watch including fire_count and last_fired_at |
222222
| `tf.delete_watch(watch_id)` | Free | Remove an owned watch |
223223
| `tf.premium_agents_directory(category=, status=, sort=, limit=, ...)` | 1 credit | Enriched directory: status, news, traffic, pricing, trending_score per agent |
224+
| `tf.news_search(q=, from_date=, to_date=, provider=, category=, limit=)` | 1 credit | Full-text news search with date/provider filters, relevance scoring, recency boost |
224225

225226
### Auto-send (requires `tensorfeed[web3]`)
226227

sdk/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "tensorfeed"
7-
version = "1.6.0"
7+
version = "1.7.0"
88
description = "Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, agent payments via USDC on Base (optional web3 auto-send)"
99
readme = "README.md"
1010
license = "MIT"

0 commit comments

Comments
 (0)