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 AXONIC console showing the End users tab for a tool named slop_list_orders. A green panel reads: Scoped — an end user can only ever reach their own data. Below it, a Bound parameters row maps the parameter customer_id to end_user.external_id, and a list headed What the model is handed shows limit, since and status as supplied by the model while customer_id is struck through and marked removed from the model's schema.
AXONIC console Build → Tools → End users cropped, not retouched

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. 1 · Activation

    Is this tool exposed to end-user sessions at all? end_user_exposed is off by default, per tool, per environment.

  2. 2 · Projection

    Bound parameters are stripped from properties, stripped from required, and additionalProperties is forced to false.

  3. 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. 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.

tool_defs.binding.scope JSON
{
  "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>"
Two sessions · identical model arguments Result
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.

Approval request · preview Dry run
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.
The run halts — it does not proceed and log a regret State
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.

The four AXONIC failure classes and what each one tells the agent to do
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.
The AXONIC call log. Filter chips across the top read Success, Retried success, Validation error, Silent failure, API error and Guardrail block. A volume panel reports 856 calls and a latency panel breaks p95 down by band, with failure counts beside each band.
AXONIC console Activity → Call log every tool call, with its outcome and its retries

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.

Retry strategy, per tool Enum
"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”.

The AXONIC reconciliation view, subtitled: writes AXONIC recorded that the provider does not agree with. A panel headed What the provider disputed shows five findings, all critical, all of kind duplicate at provider. Stat tiles below read Disagreements found 5, Critical 5, Open right now 5, Closed in window 0.
Govern → Reconciliation 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.

The AXONIC audit log. A green banner reads CHAIN INTACT above a row of interlocking chain links, with readings beside it: rows checked 348, first break none, checked just now, and a Verify again button.
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.

Tool lifecycle Status
draftin_reviewactiverejectedarchived

// The generator is allowed to be wrong, because a
// human and an eval suite stand between it and prod.
The AXONIC agent studio, Tools tab. A table headed Every tool this agent could reach lists eight SlopCRM tools — slop_create_customer, slop_create_note, slop_create_order, slop_get_customer, slop_get_order, slop_list_customers, slop_list_orders and slop_update_order_status — each tagged READ or WRITE and marked either DRAFT or ACTIVE.
AXONIC console Build → Agent studio → Tools

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.

What this costs

Your server — mint a session for the signed-in user TS
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.
Your UI — you write this part TSX
import { AxonicProvider, useAgent } from '@axonic/client/react';

const { messages, send, isStreaming,
        toolCalls, pendingApproval,
        decideApproval } = useAgent();
Generate a toolset from a spec cURL
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

$200 / month

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.