A toolbox, not a framework and not a UI builder
The layer between your agent and everything it touches.
Your agent works in the demo. Then it meets real customers, real money and a provider having a bad morning. AXONIC sits underneath and does the unglamorous half: it checks every request, keeps each customer inside their own data, stops to ask a person before anything destructive, retries without double-charging anyone, and writes down what happened. You keep the agent, the prompt and the product.
You bring the UI and the product. AXONIC brings the parts that page you at 3am.
What it is
You build the agent.
AXONIC handles what it reaches.
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
Per-customer access
Every customer sees only their own data.
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
Somebody signs off before anything is destroyed.
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.
When something breaks
Your agent is told what actually went wrong.
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. |
Retries
A retry never becomes 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 it goes and asks the provider.
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 and never quietly folded into “consistent”.
each one is a 200 OK that did not mean what it said
The audit log
A record of every privileged action, in order, that shows if anyone edits 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
Building your tools
Point it at your API. Read what it wrote before anything goes live.
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
Your test data and your customers never share a room.
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.
Includes 250,000 tool calls and 50,000 writes a month across five projects. Your usage screen shows where you are from the first call; we warn at 80% and stop at 100% rather than billing you past it in silence. Past those numbers the runtime refuses the call and tells the agent why, rather than quietly running up an invoice.
Cheaper than building it once yourself.
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.