Ask
Package ask is a plain-language question about your business, answered with real numbers.
Package ask is a plain-language question about your business, answered with real numbers.
| Base URL | https://api.hanzo.ai |
| Operations | 2 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1102 · Ask — The Grounded Answer — Draft · read the specification →
POST /v1/ask answers a plain-language question with real numbers. A question
about the caller's own business is classified to a grounded domain — books (the
ledger), projects, git (the forge inventory), or the live web — that domain's
read runs in-process under the caller's own credentials, and the model narrates
the figures it is handed, never sourcing one. It is implemented in
hanzoai/cloud at apps/ask, over the answer engine in apps/answer.
Motivation
Raw completions live at the model wire and the tool-calling orchestrator at
/v1/agent; neither promises that a figure in the answer is a value something
actually read. A founder asking "what's my MRR" needs the number to be the
ledger's, and a wrong answer to be a wrong query rather than an invention. One
door, one contract: figures are computed before any model call and returned
unaltered (apps/ask/ask.go).
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The store
It owns none. The contributor registry is built at mount; every domain is a peer asked over the internal plane, because this app ships as its own process and the data it grounds in does not live in it.
§2 Addresses
POST /v1/ask is declared with prose beside the route, not typed, and each
blocker is a wire fact measured by apps/ask/typed_wire_test.go: one route
answers two success shapes (the advisor's five-key answer and the web engine's
different eight-key one); the web branch streams server-sent events when asked
to, which no typed Out can mean; and the out-of-funds refusal is a domain body
at 402/503, not the flat typed error. The request IS declared so generated SDKs
have somewhere to put the question; the response deliberately is not, because a
single declared shape would be false for the other branch
(apps/ask/ask.go:102-132). POST /v1/ask/web is the same engine as a typed
op — the door a model can open, added because an untyped route reaches REST and
nothing else, leaving the fleet's research loop invisible to every agent in it
(apps/ask/web.go).
§3 Tenancy
A validated principal is required — 401 without one — and the gather runs AS THE
CALLER: cloud.As(c, "") carries this request's principal onto the peer call
with no widening, so a question is only ever asked about the asker
(apps/ask/ask.go:180,223). The git domain reads the forge inventory as the
caller too, because a machine read would count private repositories the member
may not open (apps/ask/git.go).
§4 Money
The capability is metered (plugin/ask/main.go, cloud.Metered; the
meteredApps row reads "the answer engine's per-question fee",
spend.go:291). The web modes debit a flat per-answer fee in cents through the
per-org ResourceMeter — Bill.Gate before the loop, MeterUsage after
(apps/answer/answer.go:135,365-372) — at the mode's default (search and news
2¢, research 25¢, apps/answer/mode.go:56-68), overridable per deployment via
CLOUD_ASK_FEE_CENTS[_<MODE>]; a negative or invalid override is ignored so a
typo can never make a paid mode free. The figure-advisor branch's one narration
completion is billed to the caller's ledger through the ai plane
(apps/ask/ask.go:277).
§5 The grounding contract
Every figure in the answer MUST be a real domain read. The Figures array is the contributor's, computed before any model call and returned unaltered; the model rewrites prose only, and with the model absent or down the deterministic template states the same figures. A gather error degrades to an honest fallback, never a guessed number. If no domain can ground the question, the advisor says so.
§6 Events, telemetry, stage, upstream
It publishes nothing to the bus. Beyond the request span it emits structured log
lines only. Stage ga: the manifest row (manifest/apps.go:436) declares no
stage, and absent means ga. It derives from no OSS upstream.
Rationale
The alternative is to let the model answer from its own memory and cite
afterwards. That reads better on the day it is wrong, which is the failure this
exists to make impossible: the narration seam is strictly read-only, so handing
a language model the ability to answer questions about a ledger costs nothing in
integrity — the same posture apps/books takes with its own ask surface.
Security Considerations
The wrong implementation is a cross-tenant oracle: an advisor that gathered with
a machine credential instead of the caller's would answer any org's revenue to
anyone signed in. The in-process replay under the caller's own principal is the
whole boundary — there is no widening parameter, so there is no place to pass a
different org. The second exposure is economic: a streaming, model-driven loop
must be admitted through the money gate before it runs, which Bill.Gate does
at the mode's fee.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | ask at its own prefix | 2 operations |
| CLI | hanzo ask … | 2 of 2 |
| SDK | AskApi in every published client | 2 methods |
| MCP | tool ask on https://api.hanzo.ai/v1/mcp | 2 operations, 1 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. This one takes arguments — the placeholders are yours to fill. POST /v1/ask, operation post_ask:
hanzo ask createimport { Configuration, AskApi } from 'hanzoai';
const api = new AskApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postAsk({ followUps: false, language: "<language>" });from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import AskApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = AskApi(client).post_ask(follow_ups=False, language="<language>")cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.AskAPI.PostAsk(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, ask_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = ask_api::post_ask(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.AskApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new AskApi(client).postAsk();curl -X POST https://api.hanzo.ai/v1/ask \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"followUps": false,
"language": "<language>"
}'The door declares no tool for ask — tools/list on https://api.hanzo.ai/v1/mcp names the products it does reach. Use HTTP or an SDK.
Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/ask/web | Research a question on the live web and answer it with sources cited |
POST /v1/ask | Ask a grounded question about your own org |
How is this guide?