# `X402.RateLimiter`
[🔗](https://github.com/cardotrejos/x402/blob/v0.9.0/lib/x402/rate_limiter.ex#L1)

Behaviour and option handling for per-wallet request rate limiting.

`X402.Plug.PaymentGate` consults a rate limiter after a payment has been
verified and before it is settled, so the key is derived from an
authenticated payer: the `from` of an unverified payload is whatever the
sender typed, and counting it earlier would let anyone exhaust a victim
wallet's allowance with forged proofs. The limit therefore bounds how many
*verified* payments a wallet can spend per window; it is not a substitute
for an edge/IP limiter against unauthenticated floods. A store implements
`c:hit/4`; `X402.RateLimiter.ETS` ships a per-node fixed-window
implementation and any shared store (Redis, a database) can be plugged in
through the same callback.

## Options

The gate's `:rate_limit` option is validated with `validate_options/1`:

* `:limit` (`t:pos_integer/0`) - Required. Maximum hits per window.

* `:window_ms` (`t:pos_integer/0`) - Required. Window length in milliseconds.

* `:key` - What is being limited: `:payer` (the verified payer address, falling
  back to the remote IP when neither the facilitator nor the verified
  scheme names one), `:ip` (the remote IP), or a 1-arity function of the
  request context (`%{conn: conn, payer: payer, payment_payload:
  payload, requirements: requirements}`) returning any term — `nil`
  exempts the request. The default value is `:payer`.

* `:store` - A module implementing `X402.RateLimiter`, or a `{module, ref}` tuple
  where `ref` is passed as the first argument of `c:hit/4`. A bare
  module is called with itself as the ref — for the default
  `X402.RateLimiter.ETS` that is the table name. The default value is `X402.RateLimiter.ETS`.

* `:on_error` - What to do when the store returns `{:error, _}` or raises: `:allow`
  lets the request through (the limiter degrades to a no-op with a
  logged warning), `:deny` answers 429 with a one-second `Retry-After`. The default value is `:allow`.

## Implementing a store

    defmodule MyApp.RedisRateLimiter do
      @behaviour X402.RateLimiter

      @impl true
      def hit(conn_name, key, limit, window_ms) do
        # INCR + PEXPIRE on "rl:" <> inspect(key), then compare with limit
      end
    end

`c:hit/4` must count the hit atomically and report either the remaining
allowance or how long the caller should wait. It should not raise for
store failures — return `{:error, reason}` and let `:on_error` decide.

# `config`

```elixir
@type config() :: %{
  limit: pos_integer(),
  window_ms: pos_integer(),
  key: :payer | :ip | (context() -&gt; term()),
  store: {module(), store_ref()},
  on_error: :allow | :deny
}
```

Validated `:rate_limit` configuration.

# `context`

```elixir
@type context() :: %{
  conn: term(),
  payer: String.t() | nil,
  payment_payload: map(),
  requirements: map()
}
```

Request context handed to a `:key` function.

# `key`

```elixir
@type key() :: term()
```

The value being rate limited.

# `result`

```elixir
@type result() ::
  {:allow, remaining :: non_neg_integer()}
  | {:deny, retry_after_ms :: pos_integer()}
  | {:error, term()}
```

Result of a hit.

# `store_ref`

```elixir
@type store_ref() :: term()
```

Store reference passed as the first argument of `c:hit/4`.

# `hit`

```elixir
@callback hit(store_ref(), key(), limit :: pos_integer(), window_ms :: pos_integer()) ::
  result()
```

Records one hit for `key` and reports whether it is within `limit` per `window_ms`.

# `check`
*since 0.9.0* 

```elixir
@spec check(config(), key()) :: {:allow, non_neg_integer()} | {:deny, pos_integer()}
```

Records a hit for `key` against the configured store.

Store errors and exceptions never propagate: they are logged and resolved
according to `:on_error` (`:allow` reports the full `limit` as remaining;
`:deny` reports a one-second retry).

# `payer`
*since 0.9.0* 

```elixir
@spec payer(map(), map() | nil, map() | nil) :: String.t() | nil
```

Recovers the verified payer address of a payment.

Prefers the `payer` the facilitator reported in its verify response
(`verify_response` is the response map or its body), and otherwise reads
the signer field authenticated by the verified scheme: EIP-3009
`authorization.from` for exact EVM, or `permit2Authorization.from` for
exact EVM with Permit2 and upto EVM. Other schemes require a reported
payer; unrelated authorization fields are ignored.

The payload's `from` is attacker-controlled until verification succeeds;
call this with a verified payment only. Pass the requirements used for
verification as the third argument, or omit it to use the payload's
verified `accepted` requirements.

## Examples

    iex> requirements = %{"scheme" => "exact", "network" => "eip155:8453"}
    iex> X402.RateLimiter.payer(%{"accepted" => requirements, "payload" => %{"authorization" => %{"from" => "0xabc"}}})
    "0xabc"

    iex> requirements = %{"scheme" => "upto", "network" => "eip155:8453"}
    iex> X402.RateLimiter.payer(%{"payload" => %{"permit2Authorization" => %{"from" => "0xdef"}}}, nil, requirements)
    "0xdef"

    iex> payload = %{"payload" => %{"authorization" => %{"from" => "0xabc"}}}
    iex> X402.RateLimiter.payer(payload, %{status: 200, body: %{"isValid" => true, "payer" => "0x123"}})
    "0x123"

    iex> X402.RateLimiter.payer(%{"payload" => %{"transaction" => "AQID"}}, %{"isValid" => true})
    nil

# `resolve_key`
*since 0.9.0* 

```elixir
@spec resolve_key(
  %{:key =&gt; :payer | :ip | (context() -&gt; term()), optional(atom()) =&gt; term()},
  context()
) :: key() | :skip
```

Resolves the rate-limit key for a request, or `:skip` when it is exempt.

`:payer` uses the payer address (lower-cased when it is a `0x` address)
and falls back to `{:ip, remote_ip}` when no verified payer is available;
`:ip` uses the remote IP; a function receives the whole context.

## Examples

    iex> context = %{conn: %{remote_ip: {127, 0, 0, 1}}, payer: "0xABC", payment_payload: %{}, requirements: %{}}
    iex> X402.RateLimiter.resolve_key(%{key: :payer}, context)
    {:payer, "0xabc"}

    iex> context = %{conn: %{remote_ip: {127, 0, 0, 1}}, payer: nil, payment_payload: %{}, requirements: %{}}
    iex> X402.RateLimiter.resolve_key(%{key: :payer}, context)
    {:ip, {127, 0, 0, 1}}

    iex> context = %{conn: %{remote_ip: {10, 0, 0, 2}}, payer: "0xabc", payment_payload: %{}, requirements: %{}}
    iex> X402.RateLimiter.resolve_key(%{key: :ip}, context)
    {:ip, {10, 0, 0, 2}}

    iex> context = %{conn: %{remote_ip: {10, 0, 0, 2}}, payer: "0xabc", payment_payload: %{}, requirements: %{}}
    iex> X402.RateLimiter.resolve_key(%{key: fn _context -> nil end}, context)
    :skip

# `retry_after_seconds`
*since 0.9.0* 

```elixir
@spec retry_after_seconds(non_neg_integer()) :: pos_integer()
```

Converts a retry delay in milliseconds to whole seconds, rounded up, for `Retry-After`.

## Examples

    iex> X402.RateLimiter.retry_after_seconds(1)
    1

    iex> X402.RateLimiter.retry_after_seconds(1_000)
    1

    iex> X402.RateLimiter.retry_after_seconds(1_001)
    2

    iex> X402.RateLimiter.retry_after_seconds(0)
    1

# `validate_options`
*since 0.9.0* 

```elixir
@spec validate_options(term()) :: {:ok, config() | nil} | {:error, String.t()}
```

Validates a `:rate_limit` option value into a `t:config/0` map.

Usable as a `NimbleOptions` custom type. `nil` disables limiting.

## Examples

    iex> X402.RateLimiter.validate_options(nil)
    {:ok, nil}

    iex> {:ok, config} = X402.RateLimiter.validate_options(limit: 10, window_ms: 60_000)
    iex> config
    %{limit: 10, window_ms: 60_000, key: :payer, store: {X402.RateLimiter.ETS, X402.RateLimiter.ETS}, on_error: :allow}

    iex> {:error, message} = X402.RateLimiter.validate_options(limit: 0, window_ms: 1)
    iex> message =~ ":limit"
    true

    iex> X402.RateLimiter.validate_options(:nope)
    {:error, "expected nil or a keyword list of rate limit options"}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
