World
A live news feed filtered to what your project cares about.
Also for this capability: API · CLI · MCP · SDKs
A live news feed filtered to what your project cares about.
| Base URL | https://api.hanzo.ai |
| Operations | 6 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1162 · World — The News Feed — Draft · read the specification →
/v1/world is a live news feed filtered to what a project cares about: GDELT
and host-allowlisted RSS/Atom normalized into one NewsItem stream, narrowed
by the project's keyword/region/source pipeline, served over REST and SSE. It
is the Go backend for the World monitor frontend (hanzoai/world), replacing
that app's edge functions with an org-scoped, in-binary subsystem. The
implementation is hanzoai/cloud apps/world.
Motivation
The World frontend ran its data plane as Vercel edge functions — an unauthenticated proxy with no tenant, no per-project config that survives, and a second deployment target beside the cloud. Moving the fetchers into the binary gives the feed an org, gives the pipeline a durable home, and puts the SSRF boundary where the rest of the estate's boundaries are enforced.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The store
One SQLite store, world (apps/world/store.go:68), holds the pipeline
config — the feed URLs to poll plus the filters applied to the merged result.
Tenancy is the (org, project) composite primary key and a mandatory
WHERE org=? AND project=? on every statement. News items themselves are
never stored: they are fetched, normalized and filtered per request, with a
ten-minute in-memory TTL cache keyed on the upstream identity (feed URL, GDELT
query) — NOT the tenant, because an upstream feed's content is a public value
identical for every org, and the per-project narrowing happens after the cache
read (apps/world/fetch.go).
§2 The address
Every route is under /v1/world (manifest/apps.go:382): six operations,
five typed. GET /v1/world/stream is the one declared route — an SSE stream
is not a value, so it carries prose beside the wire fact
(apps/world/stream.go:120). Two reads are public by design and carry no
tenant data: GET /v1/world (the front door naming the product's wires) and
GET /v1/world/limits (a plan's gates). Two more wires answer under this
prefix and are NOT served by this binary — /v1/world/mcp and /v1/world/zap
are carved off by the ingress and answered by world-gw; the generated document
cannot declare them (prose renders only for a route this router serves), so
GET /v1/world names them, and that op is the only place they appear.
§3 Tenancy
The (org, project) tuple is principal.Org + principal.Project — the values
the identity boundary minted from the validated bearer (HIP-0026) — never a
query, body or client header. A request with no validated principal is 403,
except the two public reads in §2. The SSE bus filters on org and the stream
loop drops other projects.
§4 Money
The surface is free, said in those words: cloud.Free
(plugin/world/main.go), no meter, no entry in the standing gate. What a plan
changes is limits, not price: rate and alert gates resolve from the
hanzoai/plans catalog through the one world.* entitlement vocabulary
(apps/world/entitlement.go), read, never duplicated.
§5 Events and telemetry
It publishes nothing on the bus; a customer's webhooks receive no world.*
events. The SSE stream is an in-process fan-out, best-effort and non-blocking
— a slow subscriber is dropped and re-fetches truth from GET /v1/world/news,
which MUST remain the source of truth; the stream is a live hint. Beyond the
request span it emits only its own log lines.
§6 Upstream
It derives from no third-party OSS: the fetchers are a Go port of this
product's own edge functions (hanzoai/world api/gdelt-doc.js,
api/rss-proxy.js). What it consumes are public data services — the GDELT 2.0
Doc API and the ~180 allowlisted RSS/Atom hosts, ported verbatim from the
frontend's list (apps/world/allowlist.go). Persistence is
github.com/hanzoai/sqlite (MIT / Apache-2.0 dual).
§7 Stage
beta: a vertical application (an intelligence monitor), not part of the
self-service agentic-OS core. The manifest row declares it
(manifest/apps.go:382, Stage: Beta), so the capability is reached by flag
(HIP-0139 §8).
Rationale
Filtering after a global cache, rather than caching per tenant, was chosen because the cached value is public news and the tenant-specific part is the projection: caching per org multiplies identical upstream fetches by the number of orgs and buys no isolation the filter does not already provide. The pipeline store keeps only config because news is the upstream's fact — storing items would make this a second archive that ages, instead of a view that is as fresh as its sources.
Security Considerations
The RSS fetcher is an SSRF boundary: a pipeline's feed URL is
attacker-writable config that this binary will dereference from inside the
cluster. The host MUST be on the allowlist, enforced at both the PUT write
boundary and at fetch time, including on every redirect target
(apps/world/world.go:35) — checking only at write time leaves a redirect
from an allowed host as a free pass to an internal address. The other exposure
is a cross-tenant pipeline read, closed by §3's composite key and the
never-caller-supplied org.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | world at its own prefix | 6 operations |
| CLI | hanzo world … | 6 of 6 |
| SDK | WorldApi in every published client | 6 methods |
| MCP | tool world on https://api.hanzo.ai/v1/mcp | 6 operations, 3 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/world, operation get_world:
hanzo world getimport { Configuration, WorldApi } from 'hanzoai';
const api = new WorldApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getWorld();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import WorldApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = WorldApi(client).get_world()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.WorldAPI.GetWorld(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, world_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = world_api::get_world(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.WorldApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new WorldApi(client).getWorld();curl https://api.hanzo.ai/v1/world \
-H "Authorization: Bearer $HANZO_API_KEY"Tool world, op get_world — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "world",
"arguments": {
"op": "get_world",
"input": {}
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/world/limits | Echoes a World plan's rate limits, alert quota and model-API grant, read straight from the live @hanzo/plans catalog, so agents and dashboards… |
GET /v1/world/news | Returns the caller's merged world-news feed: every source their project's pipeline names — GDELT once per keyword, plus each allowlisted RSS or Atom… |
GET /v1/world/pipeline | Returns the caller project's news pipeline: which feeds it reads and how the merged result is filtered. |
PUT /v1/world/pipeline | Replaces the caller project's news pipeline and returns what was stored. |
GET /v1/world/stream | Live news refreshes for the caller's org and project, as Server-Sent Events. |
GET /v1/world | Answers GET /v1/world — the product's public endpoint, naming every wire this surface answers on. |
How is this guide?