Skip to content

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

  1. 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.dev

    If you lose a key, create a new test key in Console → Keys. The console never shows a full key again, only its first characters.

  2. Terminal window
    npm install express ordainer
    server.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 mode unset means “do what the plan in the console says”. The sandbox plans are in enforce mode. To pin a mode in code instead, pass new Ordainer({ mode: 'shadow' }). A pinned mode overrides the console.

  3. 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 result
    curl -i -X POST localhost:3001/tools/research # call 6

    If 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 Requests
    X-Request-Id: req_FgaVvr3UBOxnF-Fb
    Retry-After: 2617689
    Content-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-After is the real number of seconds from the call until resets_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_id match the row in Console → Decisions and the export. If your clients treat 429 as “retry soon”, use new Ordainer({ denyStatus: 402 }) instead, or ordainer.middleware({ tool, denyStatus: 402 }) for one route. Only 429 and 402 are accepted; anything else throws a TypeError at startup. All the blocked responses: Errors → SDK blocked responses.

  4. 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 pilot row, 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; done

    Every 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 copying req.ordainer.decision into 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 a mode column, 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.wouldDeny is true for shadow denies. More: Shadow vs enforce.

  • 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 to middleware(). 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-Key header your caller sends. To make a retry count once, pass idempotencyKey: (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_limited with Retry-After, and onRateLimited: (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 a quota_exceeded deny for the same key and tool, that call is blocked with 429 plan_limit_reached instead of running (Sticky deny). Use failMode: 'closed' to block instead. An error answer (any 4xx, such as unknown_key) is not an outage: it’s blocked in every failMode, except 403 subscription_inactive, which follows billingLapse (default 'open': allowed), and 429 safety_limit_reached, which is allowed with result.safetyLimited set. billingLapse is 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.