Node / Express quickstart
You’ll put the Node SDK in front of one paid tool and watch it get blocked, then try shadow mode (record what would be denied, block nothing), which is how you roll out on a real plan. Switching between shadow and enforce is a console click, with no redeploy.
You need Node 20+ and an Express (or Connect-style) service. Any other framework works with ordainer.wrap() (see Next steps).
-
Sign up and copy your test key
Section titled “Sign up and copy your test key”Follow Your first deny to sign up and produce your first deny with the pilot test key. Then put the key in your shell:
Terminal window export ORDAINER_KEY=ord_test_...export ORDAINER_BASE_URL=https://api.ordainer.devIf you lose a key, create a new test key in Console → Keys. The console never shows a full key again, only its first characters.
-
Install the SDK and add the middleware
Section titled “Install the SDK and add the middleware”Terminal window npm install express ordainerserver.js const express = require('express');const { Ordainer } = require('ordainer');const ordainer = new Ordainer(); // reads ORDAINER_KEY and ORDAINER_BASE_URL. Timeout 300 ms, fails open on timeouts and outages.const app = express();app.post('/tools/research', ordainer.middleware({ tool: 'research.run' }), (req, res) => {// "ordainer" and "remaining" are only here so you can watch our decision while you try this. Leave them out of real handlers.res.json({ result: 'expensive research done', ordainer: req.ordainer.decision, remaining: req.ordainer.remaining });});app.listen(3001, () => console.log('listening on :3001'));Leaving
modeunset means “do what the plan in the console says”. The sandbox plans are in enforce mode. To pin a mode in code instead, passnew Ordainer({ mode: 'shadow' }). A pinned mode overrides the console. -
Watch your tool get blocked
Section titled “Watch your tool get blocked”Click Reset sandbox in the console, then:
Terminal window node server.js &Wait until the server prints
listening on :3001, then send the calls:Terminal window for i in $(seq 5); do curl -s -X POST localhost:3001/tools/research; echo; done # 5 x your tool's resultcurl -i -X POST localhost:3001/tools/research # call 6If curl says it failed to connect, or the loop prints blank lines, the server wasn’t ready yet. Run the calls again once you see that line.
Calls 1 to 5 return your tool’s result:
{"result":"expensive research done","ordainer":"allow","remaining":4}On call 6 your handler does not run, and the caller gets:
HTTP/1.1 429 Too Many RequestsX-Request-Id: req_FgaVvr3UBOxnF-FbRetry-After: 2617689Content-Type: application/json; charset=utf-8{"error":{"code":"plan_limit_reached","message":"This agent's plan limit is reached (0 of 5 left). It resets at 2026-11-01T00:00:00.000Z. Retrying before then will not succeed. This call was not run.","request_id":"req_FgaVvr3UBOxnF-Fb","reason":"quota_exceeded","remaining":0,"limit":5,"resets_at":"2026-11-01T00:00:00.000Z"}}Retry-Afteris the real number of seconds from the call untilresets_at, rounded up (at least 1). The message is written so an agent, or the person behind it, can read it as-is.X-Request-Id/request_idmatch the row in Console → Decisions and the export. If your clients treat 429 as “retry soon”, usenew Ordainer({ denyStatus: 402 })instead, orordainer.middleware({ tool, denyStatus: 402 })for one route. Only429and402are accepted; anything else throws aTypeErrorat startup. All the blocked responses: Errors → SDK blocked responses. -
Shadow mode for your real plan
Section titled “Shadow mode for your real plan”On a real product, start in shadow: we make the real decision and record it, but your tool runs even when we would say no. New plans you create in the console start in shadow. To see it on the sandbox: Console → Plans → on the
pilotrow, switch to shadow. Then click Reset sandbox and call 8 times:Terminal window for i in $(seq 8); do curl -s -X POST localhost:3001/tools/research; echo; doneEvery call returns your tool’s result (calls 6 to 8 with
"ordainer":"deny"). From call 6 on, your server log shows:[ordainer] shadow: would deny tool=research.run reason=quota_exceeded remaining=0/5 request_id=req_gHgqUYUyo96jVrvu (call allowed)What your caller sees: nothing different, and that’s on purpose. In shadow your tool runs and the caller gets its normal answer, HTTP 200, even on calls 6 to 8 where we would have said no. The
"ordainer":"deny"above isn’t a signal from us to your caller. It’s the sample handler copyingreq.ordainer.decisioninto the response so you can watch. Leave it out of your real handlers, because an agent will usually read a 200 with a result as “it worked”. The would-denies are for you, in your server log and in the console. When you want the agent itself to hear “no”, switch the plan back to enforce, and it gets the 429 from step 3.In the console, Overview shows “3 calls in the last 7 days would have been denied”, Decisions shows those rows as would deny (call ran) with mode
shadow, Export (CSV/JSON) has amodecolumn, and Change log lists your switch.When the would-denies look right, click switch to enforce on that plan. Your running server picks it up on the next call, with no deploy. To send decisions to your own metrics, pass
onDecision: (d) => …to the constructor:d.wouldDenyis true for shadow denies. More: Shadow vs enforce.
Next steps
Section titled “Next steps”- One agent key per customer integration or agent: create a customer, a plan and a key in the console, then pass
apiKey: (req) => keyFor(req.user)to the constructor or tomiddleware(). Counts are per key. A plan covers one tool, or every tool (*), not a list. Ordainer is in beta. For now, you set up customers, plans and keys by hand in the console. An API to do this from your own code is planned. - Retries: the middleware gives every incoming request a fresh random idempotency key and never uses an
Idempotency-Keyheader your caller sends. To make a retry count once, passidempotencyKey: (req) => …built from your own data (your job ID or queue message ID), one key per real operation. A key replays the first answer for 24 hours.retries: n(0 to 5, default 1) makes the SDK retry a timeout, network error or 503 with the same key. More: Idempotency and retries. - Rate limits: Each agent key gets about 100 requests a second, each server counts separately. Each agent key has at most 4 requests in flight at once on our hosted service (up to 20 more wait briefly). Over either, your caller gets
429 rate_limitedwithRetry-After, andonRateLimited: (d) => …runs. - Code that isn’t Express:
const research = ordainer.wrap(runResearch, { tool: 'research.run' });await research(args); // throws OrdainerDeniedError (.status / .headers / .body) when blocked
- Outages: if we’re slow (300 ms per attempt; at most about 650 ms with the default retry), unreachable or answer with a 5xx, the call is allowed and a warning is logged. The exception is a sticky deny: within 60 seconds (
stickyDenyMs) of aquota_exceededdeny for the same key and tool, that call is blocked with429 plan_limit_reachedinstead of running (Sticky deny). UsefailMode: 'closed'to block instead. An error answer (any 4xx, such asunknown_key) is not an outage: it’s blocked in everyfailMode, except403 subscription_inactive, which followsbillingLapse(default'open': allowed), and429 safety_limit_reached, which is allowed withresult.safetyLimitedset.billingLapseis the billing-lapse setting for when your own Ordainer subscription has lapsed:'open'runs calls unchecked,'closed'blocks them. See Fail open vs fail closed. - Going live: Live keys are not available yet. Live keys open when paid plans launch. Your test keys cover everything in your beta.
- Every field and error: Decide API reference and Errors.