> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zeroclick.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Ruby SDK quickstart

> Install zeroclick-sellers, mount the Rack middleware, and guard a Rails, Sinatra, or Hanami route.

The [`zeroclick-sellers`](https://rubygems.org/gems/zeroclick-sellers) gem implements the ZeroClick billing guard for Ruby backends: it verifies that each request came from ZeroClick, checks the buyer's [allowance](/concepts/usage-and-allowances) before you do the work, returns the refusal responses ZeroClick expects, and settles or reports what was used. The [integration overview](/integrate/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

```ruby theme={null}
gem "zeroclick-sellers"
```

The gem requires Ruby 3.1 or later and has **no runtime dependencies**: the core is stdlib-only (`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.

<Info>
  You need three values from the [dashboard](https://dashboard.zeroclick.io): 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](/integrate/keys-and-secrets).

  ```sh theme={null}
  ZEROCLICK_SIGNING_SECRETS=hsec_k5nq0v7m3d8p:zcsec_…
  ZEROCLICK_API_KEY=zc_…
  ```

  `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.
</Info>

## Guard a Rails route

The Railtie is opt-in, so the gem stays usable without Rails on the load path.

```ruby theme={null}
# config/application.rb
require "zeroclick/sellers/railtie"

module MyApp
  class Application < Rails::Application
    config.zeroclick.api_key = ENV.fetch("ZEROCLICK_API_KEY")
    config.zeroclick.signing_secrets = ZeroClick::Sellers.secrets_from_env

    config.middleware.use ZeroClick::Sellers::Middleware::Meter,
      service_slug: "extractor",
      usage: [ZeroClick::Sellers::UsageItem.new(meter_slug: "requests", quantity: 1)]
  end
end
```

<Warning>
  Do not pass `seller: ZeroClick::Sellers.seller` here. `config.middleware.use`
  evaluates its arguments in the `Application` class body, which runs **before**
  the initializer that reads `config.zeroclick` — so no client exists yet and
  boot fails. Omit `seller:` and the middleware resolves the process-wide client
  on the first request, by which point configuration exists. Outside Rails,
  where you build the client yourself, pass it explicitly.
</Warning>

<Note>
  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.
</Note>

Inside a controller, the verified caller is on the request env:

```ruby theme={null}
class ExtractController < ApplicationController
  def create
    zc = request.env["zeroclick.context"]
    Rails.logger.info("serving #{zc.zc_agent_id} for #{zc.zc_request_id}")
    render json: { ok: true }
  end
end
```

`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:

```ruby theme={null}
# config.ru
require "zeroclick/sellers"

SELLER = ZeroClick::Sellers.create(
  api_key: ENV.fetch("ZEROCLICK_API_KEY"),
  signing_secrets: ZeroClick::Sellers.secrets_from_env
)

use ZeroClick::Sellers::Middleware::Meter,
    seller: SELLER,
    service_slug: "extractor",
    usage: [ZeroClick::Sellers::UsageItem.new(meter_slug: "requests", quantity: 1)]

run MyApp
```

## What the middleware does

1. Reads the body (bounded at 10 MiB) and **verifies `zc-signature`** before anything else.
2. Asks ZeroClick whether the buyer can pay for the declared usage.
3. On refusal, answers `402` with the payment challenge — your app is never called.
4. On success, calls your app, then attaches `zc-usage` **only if you answered `2xx`**.

A non-2xx answer is never billed: the work was not delivered.

Your app can still read the request body. Verification has to consume it, so the middleware replaces `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:

```ruby theme={null}
request, = ZeroClick::Sellers::Middleware.request_from_env(env)

decision = SELLER.guard(
  request,
  service_slug: "extractor",
  usage: [ZeroClick::Sellers::UsageItem.new(meter_slug: "pages", quantity: pages)]
)

unless decision.allow?
  return [decision.response.status, decision.response.headers, [decision.response.body]]
end
```

For a free endpoint that still needs to know who is calling, use `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

<Columns cols={2}>
  <Card title="Configuration" icon="sliders" href="/sdks/ruby/configuration">
    Keys, the outage policy, and timeouts.
  </Card>

  <Card title="Middleware" icon="layer-group" href="/sdks/ruby/middleware">
    Meter, Identify, and settling variable usage.
  </Card>

  <Card title="API" icon="code" href="/sdks/ruby/api">
    Every public method and value type.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/sdks/ruby/errors">
    What raises, what returns a decision.
  </Card>
</Columns>
