Toolbox — not a framework, not a UI builder
The demo is the easy part.
Tool calling, schema validation, retries, idempotency, OAuth, approval gates and per-customer scoping. AXONIC is the plug-and-play layer for everything an agent gets wrong after the demo — so you can build the bespoke agent yourself, in your own UI, inside your own product.
You bring the UI and the product. AXONIC brings the parts that page you at 3am.
The scope
You still own the agent.
You stop owning the plumbing.
AXONIC does not generate your interface, does not decide your prompt, and does not own your loop. It sits underneath, between the model and everything it is allowed to touch.
- Tool generation from a spec
- JSON Schema validation
- Retries with backoff
- Idempotent writes
- OAuth and credential storage
- Per-customer parameter binding
- Human approval gates
- Dry-run request previews
- Provider reconciliation
- Hash-chained audit trail
- dev / prod separation
- Failure classification
Bound parameters
An agent cannot fetch a customer it was never shown.
You declare which parameters belong to the signed-in end user. AXONIC then does something stronger than filtering the result: it removes the parameter from the JSON Schema the model sees, so the model is never told the field exists, and fills it in from the verified session after the model has spoken.
A parameter that is absent from the schema cannot be guessed, cannot be named, and cannot be argued into by anything a user types. Prompt injection has nothing to reach for.
-
1 · Activation
Is this tool exposed to end-user sessions at all? end_user_exposed is off by default, per tool, per environment.
-
2 · Projection
Bound parameters are stripped from properties, stripped from required, and additionalProperties is forced to false.
-
3 · Rejection
If the model sends a bound parameter anyway — including a case-variant probe — the call is refused and recorded, not silently corrected. Silently winning would hide the attack.
-
4 · Injection
Only now is the real value merged in, from the verified session. It can never come from the request, a header, or a literal.
{
"bind": [
{
"param": "customer_id",
"from": "end_user.external_id"
}
],
// Default is CLOSED. An unbound read on an
// end-user-exposed tool is refused, not allowed.
"requireBoundFilter": true
}
// A source the caller could influence is rejected
// at parse time. These four are the whole list:
"end_user.id"
"end_user.external_id"
"end_user.email"
"end_user.metadata.<key>"
model asks for { "limit": 20, "status": "open" }
dana's session { …, "customer_id": "cus_dana" }
priya's session { …, "customer_id": "cus_priya" }
if dana's session sends customer_id itself:
refused scope_violation
logged model_supplied_bound_param
class needs_human
Violations surface at
GET /api/projects/:projectId/scope-violations
Approval gates
See the exact request before it is a request.
Any tool marked destructive, or carrying requireConfirmation, or caught by a guardrail, stops mid-flight and waits for a person. What that person is shown is not a summary of intent — it is the built request: the verb, the URL, the connection, and the arguments, ending in the only five words that matter.
Approvals block on a real poll, so the agent is genuinely paused rather than proceeding and filing an apology. Unattended requests expire rather than sitting open forever.
Destructive intent is detected at generation time, from the HTTP method and from the operation's own words — delete, remove, purge, revoke, cancel, refund.
create_refund (write, destructive) would
POST https://api.slopcrm.com/refunds
on connection "SlopCRM". Would create a new
refund (amount=500, order_id=107)
— no request was sent.
run.status awaiting_approval
approval.status pending
→ approved | denied | expired
// Fail-closed: if the approval row cannot
// be written, the call is refused — never
// waved through.
Failure taxonomy
“It failed” is not an instruction.
An outcome tells you what happened. A failure class tells the agent what to do next — which is a different question, and the one it keeps getting wrong. Every call AXONIC brokers is classified, and the classification is what the model is handed back.
| Class | What it means | What the agent is told |
|---|---|---|
| retryable | Transient. The provider may well answer next time. | Try again, after the backoff AXONIC computed. |
| needs_human | A person has to decide or fix something — a credential, a permission, a scope violation. | Stop. Escalate. Retrying changes nothing. |
| permanent | The request is wrong and will stay wrong. | Do not retry. Change the request or give up. |
| ambiguous | AXONIC cannot establish whether the write landed. Ambiguity outranks everything else — a write nobody can account for must never be reported as retryable, because the agent's retry is how a duplicate is born. | Read the resource back before ever repeating it. |
Writes you can account for
A retry is not a second charge.
Every write carries a fingerprint and an idempotency key that is replayed, never regenerated. When a retry is unsafe, AXONIC does not guess: it reads the resource back and adopts it if it landed, and only re-sends after proving it did not.
"none"
"header" // replay Idempotency-Key
"check_before_retry" // read back, adopt if landed
"manual" // DO NOT RETRY — report ambiguous
every write resolves to exactly one of:
response · adopted · not_landed · unknown
// `unknown` is what becomes the AMBIGUOUS class
// above — never "did not land".
Then ask the provider anyway.
A sweep re-reads what AXONIC believes it wrote and reports every disagreement: missing_at_provider, unexpected_at_provider, state_mismatch, amount_mismatch, duplicate_at_provider. The second one is the one people forget, and it is the one that produces a double charge.
Records the sweep could not confirm are counted as unverifiable — never quietly folded into “consistent”.
each one is a 200 OK that did not mean what it said
The audit log
Every privileged action, in order, each row carrying the hash of the one before it.
Rows are hashed with SHA-256 over canonical JSON and chained through prev_hash. Walking the chain reports the first break and what kind it is — content_tampered, chain_broken, duplicate_hash or genesis_mismatch.
This makes the log tamper-evident, not tamper-proof: the hash uses no secret, so anyone who can rewrite the whole table can recompute the chain. The answer is to export the head hash and anchor it somewhere AXONIC does not control. We would rather tell you that than let you assume otherwise.
GET /api/audit/verify
export as CSV or JSONL
Generation and review
Point it at a spec. Nothing it writes is callable.
Generate a toolset from an OpenAPI spec, a Postgres schema, or a plain description of what the tool should do. AXONIC infers the read/write kind, the destructive flag, the idempotency strategy and the assertions — and writes down its reasoning for each, so you are reviewing an argument rather than a black box.
Every generated tool lands as draft with eval_status: untested. Approval is refused with a 409 until its adversarial eval suite passes — no override, no query flag. Editing a behavioural field on a live tool drops it back to in_review and clears its activation.
draft → in_review → active
↘ rejected ↘ archived
// The generator is allowed to be wrong, because a
// human and an eval suite stand between it and prod.
dev / prod
The thing you test against is not the thing your customers hit.
Each project has a dev and a prod environment with their own connections, their own tools and their own API keys — pk_test and sk_test against pk_live and sk_live. Allowed browser origins are per-environment too.
A tool active in dev is not callable in prod until it is promoted, and promotion lands it back in draft — approval never rides along. Trying to activate a prod tool pointed at a dev connection is refused outright.
One honest caveat: connections, tools, keys and origins are environment-scoped; the secret vault itself is scoped to the project, and a connection binds a specific secret to a specific environment.
import { createSession } from '@axonic/client/server';
const session = await createSession({
secretKey: process.env.AXONIC_SECRET_KEY!, // server only
endUserId: user.id,
email: user.email,
metadata: { org_id: user.orgId },
ttlSeconds: 900,
});
// Everything bound to end_user.* now resolves
// from THIS session, for every tool call it makes.
import { AxonicProvider, useAgent } from '@axonic/client/react';
const { messages, send, isStreaming,
toolCalls, pendingApproval,
decideApproval } = useAgent();
curl -X POST "$BASE/connectors/generate?environment=dev" \
-H 'content-type: application/json' \
-d '{"source":"openapi","url":"…/openapi.json"}'
// → every generated tool is a draft. None is callable
// until it passes its evals and a human approves it.
Pricing
One price. Every guardrail on this page included — there is no plan where the audit log is switched off or the approval gate is an upgrade.
The alternative is paying someone six figures to build this once.
And then owning it: the retry logic, the OAuth refresh, the day the provider returns 200 for a write that never landed. AXONIC is the version you rent, keep updated, and never have to staff.