Guide · 6 min read · For engineering
Make the gate real: wiring meridian-company-os to a production spend gate
We took the spend gate from meridian-company-os, the repo behind the viral Company OS guide, and wired it to a production decision engine. Same inbox, same UX, one change: a spend approval only lands after an external policy engine says yes.
There is a viral guide going around on building a Company OS with Kimi K3, and the repo behind it, meridian-company-os, gets something right that most agent demos get wrong. BUILD 6 is an approvals inbox. The company proposes, you dispose. Every hire, spend, and override lands in a queue with policy checks attached, and when you override a failing check, you do it on purpose, in the open. BUILD 8 then puts a real agent behind a fence: when it burns through budget, it has to come ask.
Two of the principles in that guide could be lifted straight from our own docs: "Power flows through gates" and "What is not written down did not happen." We agree so hard we built a company around them.
So here is a small, honest experiment: we took Meridian's spend gate and wired it to a production decision engine. Same inbox, same UX, one change. A spend approval only lands after an external policy engine says yes.
The one change
Meridian decides approvals with a single dispatch:
dispatch({ type: "decideApproval", id: sel.id, approve: true })The integration swaps that call, for spend approvals only, with a gated version:
// Rejections and non-spend approvals behave exactly as before.
// Spend approvals only land locally after Axiru answers "allowed".
await decideApprovalGated(dispatch, sel, true);Under the hood it POSTs the approval to Axiru's decision endpoint and waits for a verdict:
const result = await requestSpendDecision(approval);
if (result.ok) {
dispatch({ type: "decideApproval", id: approval.id, approve: true });
} else {
// stays pending, with the reason rendered in the inbox
dispatch({ type: "gateApproval", id: approval.id,
gate: { provider: "axiru", status: "blocked", reason: result.reason, at: Date.now() } });
}The important design decision is what happens when things go wrong. The answer is: nothing. Fail closed, every time.
- No AXIRU_API_KEY set? Stays pending, with a message saying exactly that.
- Network down, non-200, timeout, malformed reply? Stays pending, reason shown.
- Policy says this amount needs a controller or finance leadership? Stays pending, and the reason names the role.
- Policy denies it outright? Stays pending with the policy reason and the decision id.
A gate that fails open is a decoration. The unit tests in the integration pin this: decideApproval is never dispatched unless the engine answered allowed.
One more detail worth copying even if you never touch our API: the key stays off the client. Meridian already proxies its Kimi OAuth flow through a Vite server plugin so the client bundle never sees credentials. We used the same pattern. A 40-line dev-server plugin holds AXIRU_API_KEY and forwards to POST /api/v1/decisions. The browser only ever talks to localhost.
What Axiru actually does here, and what it does not
Honesty section, because this is a reference integration, not a magic trick.
What it does: every spend approval becomes a real decision against your org's policy. Tiered approval thresholds, allow, deny, or require a named approver role. Every decision, including the denials and the failures, is written to a hash-chained receipt ledger. What is not written down did not happen. This writes it down, with hashes.
What it does not do: Axiru's v1 decision verbs today are refund, credit, and adjustment, plus USDC transfer intents for early-access orgs. There is no first-class "spend" verb yet, so this integration maps Meridian's spend approvals to adjustment and carries the Meridian context in metadata. The decision, the policy match, and the receipt are real. The verb name is the part that is still generic, and a proper spend verb is on our roadmap. Also: Meridian's other approval types (hire, publish, terminate) pass through untouched. We gated the thing our engine is actually built for.
Setup is one environment variable:
AXIRU_API_KEY=ak_your_key npm run devThe full integration is a patch against the repo: a client module, the server proxy, a reducer case, and dependency-free tests you can run with node --experimental-strip-types. MIT, like the original.
If you built a Meridian
The guide ends with a company that runs itself while you hold the gates. The next question is whether your gates would hold under a real agent with real money. Two ways to find out:
- Run our exposure scanner. It reads your agent setup and tells you where money can move without a decision being recorded.
- Connect the hosted MCP endpoint. Two minutes: point your agent at https://www.axiru.com/api/mcp with an ak_ key, and every spend intent becomes a policy decision with a receipt. OAuth support is in development.
The gate principle was right in the guide. This is what it looks like with production behind it.