zeroclick-sellers gem implements the ZeroClick billing guard for Ruby backends: it verifies that each request came from ZeroClick, checks the buyer’s allowance before you do the work, returns the refusal responses ZeroClick expects, and settles or reports what was used. The integration overview describes the contract it implements.
It ships Rack middleware, so one implementation covers Rails, Sinatra, Hanami and Roda — Rails is a Rack app.
Install
openssl, json, net/http), and the middleware is duck-typed rather than requiring rack. Adding it cannot change how anything else in your bundle resolves.
You need three values from the dashboard: your signing secret (
zcsec_…), its key id (hsec_…), and an API key (zc_…) with the usage:read and usage:write scopes. See keys and secrets.ZEROCLICK_SIGNING_SECRETS holds kid:secret pairs, comma-separated. It is a map rather than a single secret so that during a rotation both the old and the new kid verify — otherwise every in-flight request fails the moment you rotate.Guard a Rails route
The Railtie is opt-in, so the gem stays usable without Rails on the load path.In development only,
ActionDispatch::HostAuthorization sits at the top of
the Rails stack and answers 403 before the guard is reached, so testing
through a tunnel needs config.hosts << "my-tunnel.ngrok.app". It is not in
the production stack, so a deployed app needs nothing here.zc_request_id correlates with ZeroClick’s own logs — use it when you derive an idempotency key.
Guard a Sinatra or Rack route
The same middleware, mounted the Rack way:What the middleware does
- Reads the body (bounded at 10 MiB) and verifies
zc-signaturebefore anything else. - Asks ZeroClick whether the buyer can pay for the declared usage.
- On refusal, answers
402with the payment challenge — your app is never called. - On success, calls your app, then attaches
zc-usageonly if you answered2xx.
rack.input with a fresh stream over the same bytes. It deliberately does not rely on #rewind, which Rack 3 no longer guarantees.
Guard without the middleware
When the charge depends on the request, guard inside the action:Middleware::Identify or SELLER.guard_identity. Neither makes a network call.
Deployment note
The signature covers the raw, percent-encoded request target. The middleware prefers the server’s raw target (REQUEST_URI, which Puma sets) over the decoded PATH_INFO. Behind a server that decodes and exposes no raw target, a route containing an encoded separator such as %2F cannot verify — deploy behind Puma if your routes can contain one.
Next
Configuration
Keys, the outage policy, and timeouts.
Middleware
Meter, Identify, and settling variable usage.
API
Every public method and value type.
Errors
What raises, what returns a decision.