Implementation

Architecture for engineers.

The technical concreteness pass. What you'll wire, what scopes we ask for, how long it takes, and what happens when Axiru is unreachable. No marketing arm-waving.

Time to integrate

From connect to enforce in two weeks.

Most teams reach enforcing-on-refunds in ~10 hours of engineering across two weeks. The first week is policy tuning in shadow mode; the second is flipping the switch.

Day 0 — Connect Stripe

OAuth into Stripe with read_only base scope. Axiru pulls 90 days of history into shadow-mode replay. No code changes in your app.

~5 minutes

Day 1–3 — Shadow review

Walk the replay with your team. Tune policies on observed history before any line of code is changed. Free for 90 days.

~30 minutes/day

Week 1 — Wire the decide call

Add the ~10-line decide call in front of refunds (or your first chosen action). Default is observe-only — the call returns advisory, your code still executes normally.

1–2 hours of engineering

Week 2 — Flip to enforce

Once shadow telemetry shows the policy is doing what you want, flip the action to enforce. Deny outcomes now block the Stripe call.

Policy review + a feature flag

Week 3+ — Expand rails

Payouts, transfers, app-fee refunds, disputes — same decide shape, different `action`. Onboard one at a time.

Same pattern per action
The decision call

One authenticated POST in front of every outflow.

The integration shape is intentionally boring. Bearer auth, idempotency-keyed, allow/deny/review semantics. If you can call Stripe, you can call Axiru.

HTTP / REST

From your backend, before stripe.refunds.create.

// Before issuing a refund, ask Axiru if it should happen.
const decision = await fetch("https://api.axiru.com/api/v1/decisions", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.AXIRU_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    intent: {
      kind: "refund",                  // refund | credit | adjustment
      amount_cents: 12_500,
      currency: "USD",
      stripe_connection_id: connId,
      metadata: { order_id },
    },
    context: {
      customer_id: customerId,
      customer_tenure_days: 412,
      reason_code: "requested_by_customer",
    },
    idempotency_key: refundRequestId,  // safe to retry
  }),
}).then((r) => r.json());

if (decision.outcome === "allow") {
  await stripe.refunds.create({ charge: chargeId, amount: 12_500 });
} else if (decision.outcome === "deny") {
  return { error: decision.reason_code, message: decision.message };
} else if (decision.outcome === "review") {
  return { pending: true, approval_id: decision.approval_id };
}

MCP tool

From an LLM agent, via the axiru.decisions.create MCP tool.

// MCP-flavored equivalent — same payload, agent-callable.
const result = await mcp.callTool("axiru.decisions.create", {
  intent: {
    kind: "refund",
    amount_cents: 12_500,
    currency: "USD",
    metadata: { order_id },
  },
  context: {
    customer_id: customerId,
    reason_code: "requested_by_customer",
  },
  idempotency_key: refundRequestId,
});

// result.outcome in { "allow", "deny", "review" }
// result.policy_version, result.decision_id

Full request/response reference lives in the decisions API docs. Every decision returns a decision_id and policy_version that land in the decision ledger for audit. Default rate limit is 100 requests/minute per API key, fail-closed when Redis is unreachable.

Integration shapes

Three ways to plug in.

Most customers start with the REST decide call on refunds. Agent-heavy teams prefer the MCP tool. Webhook is the observe-only fallback for legacy code paths.

REST API

POST /api/v1/decisions with a Bearer API key. Returns within 80ms p95. Use this if you're calling from your own backend.

  • Bearer auth via per-environment API keys (ak_... prefix).
  • `idempotency_key` in the body — safe to retry the same decision request.
  • Response includes decision_id, policy_version, outcome, and (if review) approval_id.

MCP tool

Same payload exposed as `axiru.decisions.create` over MCP. Use this if an LLM agent is the caller and you don't want it touching Stripe directly.

  • Agent gets allow/deny/review, never a raw Stripe key.
  • The agent's tool-call args become the decision context for audit.
  • Pairs with the AGT browser extension for human-in-the-loop reviews.

Webhook (Path B — observe)

Drop-in option for legacy code paths where adding a pre-flight call isn't practical. We observe and alert on the resulting Stripe event, but we cannot block.

  • Stripe → Axiru webhook subscription, no code change in your app.
  • Decision ledger entries land within seconds of the event.
  • No enforcement — for enforcement you need Path A (the decide call).
Required scopes

What we ask Stripe for. And why.

We follow least-privilege. Read-only is enough to start; write scopes are requested only for the actions you choose to enforce.

read_only base
Day 1, all customers.

Inventory accounts, balances, charges, refunds, payouts, app fees.

Required for shadow-mode replay and audit. No money is moved with read-only scopes.

refunds:write
When you flip refunds from observe to enforce.

Create and decline refunds via the Refunds API.

Required for refund enforcement. Without it, Axiru can decide but not execute.

payouts:write
When you flip payouts to enforce (typically week 3+).

Create, reverse, or hold payouts to connected accounts.

Required for payout enforcement and emergency holds.

transfers:write
Marketplace customers only; when you flip transfers to enforce.

Create or reverse Connect transfers between platform and connected accounts.

Required for marketplace transfer enforcement and clawbacks.

disputes:write
When Evidence Agent is enabled (optional).

Respond to and submit evidence for disputes.

Required when Evidence Agent submits evidence packets on your behalf.

application_fees:write
Platform customers using app fees.

Refund application fees on Connect charges.

Required for app-fee refund enforcement on platform charges.

Tokens are stored encrypted and rotated on the key-rotation rehearsal cadence. We never request a scope until the corresponding action is being enforced.

Fail-open vs fail-closed

What happens if Axiru is unreachable.

Money-path infrastructure cannot quietly fail. Defaults are conservative — money-out actions hold; advisory and time-sensitive actions degrade gracefully. Every default is per-policy overridable.

refunds

Money leaving the business. If Axiru is unreachable, the refund is held for human review rather than auto-executed. Configurable per policy.

fail-closed
payouts

Money leaving the platform to connected accounts. Default hold on unreachable; explicit allow-list to fail-open per program.

fail-closed
transfers

Connect transfers between platform and connected accounts. Default hold; same per-policy override available.

fail-closed
application_fee_refunds

App-fee refunds reduce platform revenue. Hold on unreachable.

fail-closed
dispute_evidence_submission

Missing the Stripe response window is worse than submitting on best-available evidence. Evidence Agent falls back to the last good packet and emits a degraded-mode signal.

fail-open
shadow-mode advisory (decide)

Pre-enforce, the decide call is advisory only. A failed advisory never blocks your code path.

fail-open

We test these defaults with a continuous fail-closed contract suite (T-044, shipped). The roadmap tracks expansion of that suite to every new rail before it ships.

Next step

Connect Stripe, run shadow mode, decide later.

The first 90 days are free and read-only. You'll have replayed history and tuned policy before you ever add a line of code.

Start in shadow mode first. Move to live enforcement later.

See it run →