On this page
There is no Ruby SDK. For an API with one endpoint that matters, that is less of an obstacle than it sounds: the wrapper is a few dozen lines of Net::HTTP, and writing it yourself means the timeout and the failure mode are decisions you made rather than defaults you inherited.
What follows is the shape we would actually deploy in a Rails app: the form markup, a client object, a controller concern, and different strictness per action. It follows the three steps the Maskbreak console lists (add the script to the page with your form, send the check from your server, submit your form once) and starts in watch mode, where every check is logged and nothing is refused.
Step 1: Add the script to the page with your form
Load the script in your layout and put class="monocle-enriched" on the form itself:
<%# app/views/layouts/application.html.erb, in <head> %>
<script async src="https://maskbreak.com/assets/sentinel.js"></script>
<%# app/views/registrations/new.html.erb %>
<%= form_with url: registrations_path, class: "monocle-enriched" do |f| %>
<%= f.email_field :email, required: true %>
<%= f.password_field :password, required: true %>
<%= f.submit "Create account" %>
<% end %>
The script adds three hidden fields to the form: monocle (the network token), sentinel_fp (the browser check id) and sentinel_tz (the time zone). They arrive as ordinary top-level params. With Turbo Drive, a page reached by a Turbo visit is swapped in without a full page load, so the script never fills the form on it and the post arrives without monocle. Make the page with the form load fully: put <meta name="turbo-visit-control" content="reload"> in that page's <head>, or data-turbo="false" on the links that lead to it.
If you set a Content Security Policy in config/initializers/content_security_policy.rb, add https://maskbreak.com, https://mcl.maskbreak.com, https://*.mcl.maskbreak.com, https://mesu.maskbreak.com and the string "'wasm-unsafe-eval'" to script_src, the mcl hosts (https and wss) and mesu to connect_src, and https://mesu.maskbreak.com plus :blob to worker_src.
Step 2: Send the check from your server
One class, one method, standard library only. It sends whatever evidence arrived, including none: a check without a token or a browser check id is how the console tells you which half is missing. It never raises.
# app/services/maskbreak.rb
require "net/http"
require "json"
class Maskbreak
ENDPOINT = URI("https://maskbreak.com/v1/evaluate").freeze
MODE = ENV.fetch("MASKBREAK_MODE", "watch") # "watch" or "enforce"
def initialize(api_key: ENV.fetch("MASKBREAK_API_KEY", ""), timeout: 3)
@api_key = api_key
@timeout = timeout
end
# Returns the verdict (a Hash), or nil when there is none. Never raises.
def check(params, email: nil, account_id: nil)
body = {
token: params[:monocle].presence || params[:token].presence || "",
fingerprintEventId: params[:sentinel_fp].presence || params[:fingerprintEventId].presence,
tz: params[:sentinel_tz].presence || params[:tz].presence,
email: email,
accountId: account_id
}.compact
req = Net::HTTP::Post.new(ENDPOINT)
req["Authorization"] = "Bearer #{@api_key}"
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
res = Net::HTTP.start(ENDPOINT.host, ENDPOINT.port, use_ssl: true,
open_timeout: @timeout, read_timeout: @timeout) { |http| http.request(req) }
result = res.is_a?(Net::HTTPSuccess) ? JSON.parse(res.body) : nil
Rails.logger.warn("maskbreak HTTP #{res.code}") unless result
result
rescue StandardError => e # timeouts, refused connections, TLS errors, a body that is not JSON
Rails.logger.warn("maskbreak unavailable: #{e.class}")
nil
ensure
# One line per check. Never log the key, the token or form data.
Rails.logger.info("maskbreak mode=#{MODE} decision=#{result&.dig('decision').inspect} " \
"reasons=#{result&.dig('reasons').inspect} " \
"device_evidence=#{result&.dig('device_evidence').inspect} " \
"degraded=#{result&.dig('degraded').inspect}")
end
# A complete, production allow: what a strict action needs in enforce mode.
def self.clean_allow?(v)
v.present? && v["decision"] == "allow" && v["device_evidence"] == "ok" &&
!v["degraded"] && !v["test"]
end
end
Both timeouts are set explicitly: Ruby's defaults are measured in tens of seconds, which is not a number you want in front of a checkout button. StandardError covers Net::OpenTimeout and Net::ReadTimeout along with every other way the call can fail, so a Maskbreak problem never becomes an error page of yours. Put your live key in MASKBREAK_API_KEY (Rails credentials or your host's secrets), never in the repository.
Wire it into your own app: a free key returns decision, risk_score and reasons for every visit, 10,000 checks a month on the Free plan, no card.
Get an API keyThe controller concern
Rails already has the right shape for this: a concern with a before_action, included where it is needed and nowhere else. In watch mode it records the verdict and never renders anything.
# app/controllers/concerns/fraud_screened.rb
module FraudScreened
extend ActiveSupport::Concern
included do
attr_reader :verdict
end
class_methods do
# screen_with strict: true → enforce mode continues only on a clean allow
# screen_with → enforce mode refuses only a block
def screen_with(strict: false, **options)
before_action(**options) { screen!(strict: strict) }
end
end
private
def screen!(strict:)
email = params[:email].is_a?(String) ? params[:email] : nil
@verdict = Maskbreak.new.check(params, email: email, account_id: current_user&.id&.to_s)
return unless Maskbreak::MODE == "enforce"
if @verdict&.dig("decision") == "block"
render json: { error: "Request declined" }, status: :forbidden
elsif strict && !Maskbreak.clean_allow?(@verdict)
render json: { error: "Additional verification required" }, status: :conflict
end
end
end
Per-action strictness
Now the policy is one readable line per controller, and it lives next to the actions it governs.
class WithdrawalsController < ApplicationController
include FraudScreened
screen_with strict: true, only: %i[create] # money leaving: nothing but a clean allow
end
class RegistrationsController < ApplicationController
include FraudScreened
screen_with only: %i[create] # signup: blocks only
end
class CheckoutsController < ApplicationController
include FraudScreened
screen_with only: %i[create]
def create
return step_up! if Maskbreak::MODE == "enforce" && verdict&.dig("decision") == "review"
charge!
end
end
The reason signup is not strict: a plain VPN returns review, and VPN users are customers. Refusing them at registration is a cost you never see, because the person who could not sign up does not file a support ticket. The hard signals (proxy, Tor, automation, emulator, browser tampering) are the ones that return block.
Step 3: Submit your form once
Run the app on your computer or deploy it, open the page in an ordinary browser, wait a moment, and submit the form. The first check then appears in the Maskbreak console (if half of it did not arrive, the console says which half), and your log shows one maskbreak line. degraded=true means no network token arrived (the script was blocked, the class is missing, or a Turbo visit swapped the page in); device_evidence="not_sent" means no browser check id arrived; "ok" with degraded not true is a complete check. When the lines look right after a few days, set MASKBREAK_MODE=enforce and redeploy.
Do not do this in a background job
The temptation in Rails is to push the network call into Sidekiq and keep the request fast. It does not work for a gate: by the time the job runs, the account exists, the order is placed, or the withdrawal is queued. The verdict has to be in the request path if it is going to change what happens.
What does belong in a job is everything after the decision: writing the verdict to your own analytics table, updating a risk queue, sending the alert. Decide inline, record asynchronously. Watch mode is the honest way to measure the latency first: the call is inline, and nothing is acted on until you switch.
Testing
The public sandbox key returns fixed answers for the test tokens (test_clean allow, test_vpn review, test_proxy and test_tor block), each marked test: true. Request specs then cover the block path:
# spec/requests/withdrawals_spec.rb
before do
stub_const("Maskbreak::MODE", "enforce")
allow(ENV).to receive(:fetch).and_call_original
allow(ENV).to receive(:fetch).with("MASKBREAK_API_KEY", "").and_return("sk_test_sandbox")
end
it "refuses a withdrawal from a proxy" do
post "/withdrawals", params: { amount: 100, monocle: "test_proxy" }
expect(response).to have_http_status(:forbidden)
end
it "holds the withdrawal when the vendor is down" do
allow_any_instance_of(Maskbreak).to receive(:check).and_return(nil)
post "/withdrawals", params: { amount: 100 }
expect(response).to have_http_status(:conflict)
end
That second test documents your actual policy for money leaving: no verdict, no payout without a human. On signup the same outage lets the request through. Write both down as assertions so nobody quietly changes them in six months.
The short version
- Script in the layout,
class: "monocle-enriched"on the form, three hidden fields forwarded astoken,fingerprintEventIdandtz. - No gem needed. A few dozen lines of
Net::HTTP, both timeouts set explicitly, every error rescued. - Call every time, even without a token, and log one line. Watch mode never refuses.
- A concern plus
before_actionkeeps the policy readable and per-controller. - In enforce mode: strict on money, blocks only on signup,
reviewto a step-up rather than a refusal. - Decide inline; write the audit trail from a job.
Questions people ask
- Is there a Ruby SDK for Maskbreak?
- No. The official SDKs are Node, Python and PHP. For Ruby the integration is a small Net::HTTP wrapper — about sixty lines including error mapping — which is enough for an API with one endpoint that matters, and it leaves the timeout and failure policy in your own code.
- Should the fraud check run in a Sidekiq job?
- Not as a gate. By the time a background job runs, the account already exists or the order is already placed, so the verdict cannot change the outcome. Make the call inline with a short timeout, and use a job for what comes after the decision: analytics writes, risk queues, alerts.
- Why does my log show degraded=true on every check?
- The network token is not reaching your server. Check, in order: the script tag is on the page with the form; class "monocle-enriched" sits on the form element itself, not on an input; a Content-Security-Policy is not blocking maskbreak.com or its hosts (the browser console shows CSP blocks); and the page was not reached by a Turbo visit, which swaps the page in without the token.
- What is the right timeout for a fraud check in Rails?
- Short: the guide uses three seconds on both open_timeout and read_timeout. Ruby's defaults are measured in tens of seconds, which is long enough for a vendor slowdown to become an outage of your own checkout. Pair the short timeout with a rescue that lets the request continue.
Paste it in, then watch the verdicts
The public sk_test_sandbox key returns the documented allow, review and block shapes with no account, so the failure path is testable before you go live. SDKs for Node, Python and PHP, or plain HTTP. The API reference and the Free plan cover the rest.



