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