Skip to content

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.

  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_API_KEY=ord_test_...
    export ORDAINER_BASE_URL=https://api.ordainer.dev

    ORDAINER_KEY (the name the console shows) works with this gem too. If ORDAINER_API_KEY is 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.

  2. The gem is called ordainer and is on RubyGems. Add it to your Gemfile and run bundle 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"]
    end

    Agents 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 with 422 before Ordainer sees it).

    app/controllers/tools_controller.rb
    class ToolsController < ApplicationController
    skip_forgery_protection
    include Ordainer::Rails::Guard
    ordainer_check tool: "research.run", only: :research
    def 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 }
    end
    end
    config/routes.rb
    post "/tools/research", to: "tools#research"

    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, set c.mode = :shadow in the initializer. A pinned mode overrides the console.

  3. 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 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 action does not run, and the caller gets:

    HTTP/1.1 429 Too Many Requests
    X-Ordainer-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-Ordainer-Request-Id / request_id match the row in Console → Decisions and the export (Rails uses X-Request-Id for its own request ID). If your clients treat 429 as “retry soon”, set c.deny_status = 402 instead. Only 429 and 402 are accepted; anything else raises Ordainer::ConfigurationError 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 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 copying ordainer_decision.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 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 a mode column, 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.

  • 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 } to ordainer_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-Key header your caller sends. To make a retry count once, pass idempotency_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_limited with Retry-After.
  • Code that isn’t a controller:
    client = Ordainer::Client.new # reads ORDAINER_API_KEY (or ORDAINER_KEY) and ORDAINER_BASE_URL
    client.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 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 c.fail_mode = :closed to block instead. An error answer (any 4xx, such as unknown_key) is not an outage: it’s blocked in every fail_mode, except 403 subscription_inactive, which follows c.billing_lapse (default :open: allowed), and 429 safety_limit_reached, which is allowed with decision.safety_limited set. billing_lapse 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.