Ruby / Rails quickstart
You’ll put the Ruby gem in front of one paid tool in a Rails app 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 Ruby 3.0+ and a Rails app (Rails 7 or later). Plain Ruby works too, with client.decide (see Next steps). The gem has no dependencies outside Ruby’s standard library.
-
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_API_KEY=ord_test_...export ORDAINER_BASE_URL=https://api.ordainer.devORDAINER_KEY(the name the console shows) works with this gem too. IfORDAINER_API_KEYis also set, that one is used.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.
-
Install the gem and add the guard
Section titled “Install the gem and add the guard”The gem is called
ordainerand is on RubyGems. Add it to yourGemfileand runbundle install:Gemfile gem "ordainer", "~> 0.1"Or, outside a Gemfile:
gem install ordainer.config/initializers/ordainer.rb # Reads ORDAINER_BASE_URL too. Timeout 0.3 s, fails open on timeouts and outages.Ordainer.configure do |c|c.api_key = ENV["ORDAINER_API_KEY"] || ENV["ORDAINER_KEY"]endAgents call this endpoint with a key, not from a browser form, so turn off Rails’ form-token check for this controller with
skip_forgery_protection(without it, a new Rails app answers the call with422before Ordainer sees it).app/controllers/tools_controller.rb class ToolsController < ApplicationControllerskip_forgery_protectioninclude Ordainer::Rails::Guardordainer_check tool: "research.run", only: :researchdef research# "ordainer" and "remaining" are only here so you can watch our decision while you try this. Leave them out of real handlers.render json: { result: "expensive research done", ordainer: ordainer_decision.decision, remaining: ordainer_decision.remaining }endendconfig/routes.rb post "/tools/research", to: "tools#research"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, setc.mode = :shadowin the initializer. 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 bin/rails server -p 3001 &Rails can take a few seconds to boot. Wait until it prints a line starting
* Listening on, 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 action does not run, and the caller gets:
HTTP/1.1 429 Too Many RequestsX-Ordainer-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-Ordainer-Request-Id/request_idmatch the row in Console → Decisions and the export (Rails usesX-Request-Idfor its own request ID). If your clients treat 429 as “retry soon”, setc.deny_status = 402instead. Only429and402are accepted; anything else raisesOrdainer::ConfigurationErrorat 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 Rails 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 controller copyingordainer_decision.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 output 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 app picks it up on the next call, with no deploy. To send decisions to your own metrics, set
c.on_decision = ->(d) { … }in the initializer:d.would_deny?is 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
customer: ->(c) { c.current_account.ordainer_key }toordainer_check. 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 guard gives every incoming request a fresh random idempotency key and never uses an
Idempotency-Keyheader your caller sends. To make a retry count once, passidempotency_key: ->(c) { … }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.c.retries = n(0 to 5, default 1) makes the gem 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 and 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. - Code that isn’t a controller:
client = Ordainer::Client.new # reads ORDAINER_API_KEY (or ORDAINER_KEY) and ORDAINER_BASE_URLclient.guard(tool: "research.run") { run_research(args) } # raises Ordainer::BlockedError (#status / #headers / #body) when blocked
- Outages: if we’re slow (0.3 s per attempt; at most about 0.65 s 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 (
sticky_deny) of aquota_exceededdeny for the same key and tool, that call is blocked with429 plan_limit_reachedinstead of running (Sticky deny). Usec.fail_mode = :closedto block instead. An error answer (any 4xx, such asunknown_key) is not an outage: it’s blocked in everyfail_mode, except403 subscription_inactive, which followsc.billing_lapse(default:open: allowed), and429 safety_limit_reached, which is allowed withdecision.safety_limitedset.billing_lapseis the billing-lapse setting for when your own Ordainer subscription has lapsed::openruns calls unchecked,:closedblocks 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.