Label
Package label is the ground-truth plane: what actually turned out to be fraud, who said so, and when they could first have said it.
Package label is the ground-truth plane: what actually turned out to be fraud, who said so, and when they could first have said it.
| Base URL | https://api.hanzo.ai |
| Operations | 7 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1261 · Label — Ground Truth — Draft · read the specification →
/v1/label is the ground-truth plane of the risk product: what actually turned
out to be fraud, who said so, and when they could first have said it. It closes
the loop the decision plane cannot close for itself — /v1/risk decides, and
the answer key arrives late, from several places, sometimes in disagreement:
a charge-off, an adjudicated dispute, a closed compliance case, a fraud-reason
refund, an analyst's review, a judged sample (apps/label/label.go:1-13). It is
implemented in hanzoai/cloud at apps/label. This HIP is where HIP-1046 §6's
subtree now lives; HIP-1046 keeps the invariants that hold across the planes.
Motivation
A label that fed an adverse action is a compliance record. Its writers are
mostly not the decision plane — commerce adjudicates the dispute, the
compliance face closes the case, an analyst files the review — its readers are
the dataset materialiser and the evaluator, and its retention clock is its own:
the label's life is not the life of the decision that cited it
(apps/label/label.go:55-63). Own writers, own readers, own clock is why this
is its own capability rather than a subtree of risk.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
The store
The record is the tenant's own encrypted SQLite file, opened per organization
through cloud.OrgStore under the name label (apps/label/label.go:158).
hanzo.risk_label in the shared warehouse is a DERIVED mirror for joining at
training scale (apps/label/mirror.go:85, :213): the record is written
first, and the mirror's failure is reported rather than fatal. Nothing here
rides the best-effort event path.
Every writing operation ships the tenant's file to its durable object before it
answers, and an unacked ship fails the request (apps/label/label.go:259).
This is not optional: the deployment recreates the process on every rollout and
the successor hydrates the durable snapshot OVER the local file, so a write
acknowledged but not shipped would be overwritten by an older copy of the same
tenant's history on the ordinary path (apps/label/label.go:48-54).
The address
The capability answers under /v1/label: filing and listing assertions at the
root, resolve, coverage, vocabulary, hold and dispose
(apps/label/typed.go:165-189). Every operation is typed. The name is
singular by HIP-0139 §2.2 — there is no /{id} member route, so this is a
faculty; if a per-assertion read is ever proposed, the name question MUST be
settled before that route ships, because a later flip to labels is a second
wire break. Today's router still serves this surface under /v1/risk/labels;
that pair is carried by hanzoai/cloud openapi/misfiled.txt and closes by
fold.
The record's three properties
- Latency. Every assertion carries when the event happened, when the filer
says it became knowable, and the DERIVED instant — the later of the filer's
claim and the server clock at the write, never supplied
(
apps/label/label.go:17-27). Resolution takes an observation instant and shows only what was knowable then; the guard MUST read the derived instant, because a guard whose only input is a caller-chosen value is exactly as strong as the caller's honesty. - Conflict. Assertions that disagree BOTH stay; a total order over
adjudication weight picks the one in force and returns the losers beside it
(
apps/label/resolve.go:146). There is no UPDATE statement in the package. - Provenance. Every assertion names its source, its evidence, and the
identity that filed it, the last stamped server-side from the validated
principal; a label with no evidence is refused at the door
(
apps/label/label.go:35-41).
hold places the record behind an answer under litigation hold by its content
digest (apps/label/typed.go:189, apps/label/zipdoc_gen.go:151); dispose
is the tenant's own retention decision. A held record MUST NOT be disposable
while the hold stands.
Tenant, meter, events, observability, stage
The organization comes from the validated principal
(apps/label/label.go:196) and a request without one is refused with 403
(apps/label/label.go:194-198); the per-org file makes the cross-tenant read
unspellable rather than merely forbidden. The capability is free, in those
words (plugin/label/main.go:29, Price: cloud.Free). It publishes no events
on the bus. Beyond the request span it registers nothing; the mirror's failure
is reported in the answer rather than counted in a private metric. Its stage
is beta.
Upstream
It derives from none: no OSS project is forked, embedded or mirrored.
Rationale
The alternative is a labels table inside the decision plane's store. It is
one fewer process and it braids two lifetimes: retention policy written for
decisions would silently govern compliance records, and the decision plane's
single-writer store would gain a second class of writer. A separate per-tenant
file keeps one owner per file and lets the record outlive the decisions it
judges.
The mirror exists because training-scale joins over thousands of per-tenant files are not a query; it is derived and second so that losing it loses nothing.
Security Considerations
This plane feeds adverse decisions about people, so its integrity is the attack surface. A forged or back-dated label poisons every training set built after it — which is why the knowable instant is server-derived and the filing identity is stamped from the principal, never taken from the body. A deleted contested label would resolve a dispute into silence — which is why conflicting assertions both stay and there is no UPDATE. A cross-tenant read is one organization's fraud history handed to another — which is why the tenant is a file, not a predicate. And a record that vanished before litigation ended is spoliation — which is why hold outranks dispose.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | label at its own prefix | 7 operations |
| CLI | — | no command reaches it yet — use HTTP or an SDK |
| SDK | LabelApi in every published client | 7 methods |
| MCP | tool label on https://api.hanzo.ai/v1/mcp | 7 operations |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/label, operation riskLabels:
hanzo has no subcommand for this operation — the CLI serves only what cloud's live route table confirms. Use HTTP or an SDK.
import { Configuration, LabelApi } from 'hanzoai';
const api = new LabelApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.riskLabels();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import LabelApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = LabelApi(client).risk_labels()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.LabelAPI.RiskLabels(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, label_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = label_api::risk_labels(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.LabelApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new LabelApi(client).riskLabels();curl https://api.hanzo.ai/v1/label \
-H "Authorization: Bearer $HANZO_API_KEY"Tool label, op riskLabels — 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": "label",
"arguments": {
"op": "riskLabels",
"input": {}
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/label/coverage | How much of the window has matured, and how much of that is judged |
POST /v1/label/dispose | Dispose of this tenant's expired assertions, whole records only |
POST /v1/label/hold | Place or release a litigation hold on named records |
POST /v1/label/resolve | Resolve the label in force for named events, as of each event's own horizon |
GET /v1/label/vocabulary | The closed vocabularies and the precedence rule that resolves a conflict |
GET /v1/label | Read the assertions this tenant has recorded |
POST /v1/label | Assert ground truth about events |
How is this guide?
Dataset
Package dataset is the per-org dataset plane of /v1/risk: a dataset is a VERSIONED, IMMUTABLE snapshot of one tenant's own event surface, and this is where it is declared, materialised, described,…
Reference
Package reference is the lookup data a risk decision needs but cannot derive: which email domains hand out throwaway inboxes, which addresses belong to a datacentre or a Tor exit, which card scheme…