# Ruby bot protection for Rails and Sinatra | Nolgard

> Verify Nolgard tokens from Ruby with Net::HTTP. A Rails controller that calls /v1/verify, and how to do the same in Sinatra.

Source: https://nolgard.com/docs/ruby/
Last updated: 2026-10-25

[Docs](https://nolgard.com/docs/) / Ruby

Guide

# Ruby

A Rails controller using `Net::HTTP` from the standard library. No extra gems.

## Browser

```
<script src="https://nolgard.com/v1/client.js" data-site-key="SITE_KEY" defer></script>
<script>
  document.querySelector('#signup').addEventListener('submit', async (event) => {
    event.preventDefault();
    let token; // stays undefined when the script is blocked: the server takes the missing-token path
    try {
      await Nolgard.ready;
      token = await Nolgard.execute({ action: 'signup' });
    } catch {}
    const form = event.target;
    const response = await fetch('/signup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, email: form.elements.email.value })
    });
    // Show your own success or error message here.
  });
</script>
```

## Rails

```
# app/controllers/signups_controller.rb
# config/routes.rb: post "/signup", to: "signups#create"
require "json"
require "net/http"

class SignupsController < ApplicationController
  VERIFY_URL = URI("https://nolgard.com/v1/verify")
  UNAVAILABLE = { "success" => false, "error" => "unavailable" }.freeze

  def create
    email = params[:email].to_s
    result = nolgard_verify(params[:token].to_s, "signup", { email: email })

    unless result["success"]
      case result["error"]
      when "invalid-secret", "invalid-input"
        return render json: { error: "try-again" }, status: 503 # config error: fail closed
      when "missing-token" # script blocked: not a bot verdict
        create_account(email, trial_days: 3, confirm_email_first: true)
        return render json: { ok: true, confirm_email: true }
      when "unavailable", "over-quota" # fail open with a reduced grant
        create_account(email, trial_days: 3)
        return render json: { ok: true }
      else
        return render json: { error: "retry-token" }, status: 409 # stale token: the form retries once
      end
    end

    case result["decision"]
    when "allow"
      create_account(email)
    when "limit"
      create_account(email, trial_days: 3)
    when "shadow"
      # same response, nothing created
    when "challenge"
      return render json: { error: "confirm-email" }, status: 403
    else
      return render json: { error: "rejected" }, status: 403
    end
    render json: { ok: true }
  end

  private

  def nolgard_verify(token, action, account = nil)
    payload = {
      secret: ENV.fetch("NOLGARD_SECRET"),
      token: token,
      remoteIp: request.remote_ip,
      action: action
    }
    payload[:account] = account if account
    begin
      response = Net::HTTP.start(VERIFY_URL.host, VERIFY_URL.port, use_ssl: true,
                                 open_timeout: 3, read_timeout: 3, write_timeout: 3) do |http|
        http.post(VERIFY_URL.path, payload.to_json, "Content-Type" => "application/json")
      end
      return UNAVAILABLE if response.code.to_i >= 500
      JSON.parse(response.body)
    rescue Timeout::Error, SocketError, SystemCallError, IOError, OpenSSL::SSL::SSLError,
           Net::HTTPBadResponse, JSON::ParserError
      UNAVAILABLE # timeout, network error, 5xx or bad JSON
    end
  end
end
```

Supply your own private `create_account` method; `confirm_email_first` holds the trial until the user clicks your confirmation link. Without a token (ad blocker, Brave Shields, no JavaScript) the user goes through email confirmation instead of a rejection. Keep Rails' CSRF protection on: send the token from `csrf_meta_tags` in an `X-CSRF-Token` header with the form's fetch. `request.remote_ip` skips the proxies listed in `config.action_dispatch.trusted_proxies`; add your own proxy there. Do not trust unchecked forwarding headers.

## Sinatra

The same code works in a Sinatra `post "/signup"` route. Put `nolgard_verify` in a `helpers` block, read the body with `JSON.parse(request.body.read)`, use `request.ip` for `remoteIp`, and replace each `render json:` with `halt status, body.to_json`.
