# `X402.Extensions.PaymentIdentifier.RedisCache`
[🔗](https://github.com/cardotrejos/x402/blob/v0.9.0/lib/x402/extensions/payment_identifier/redis_cache.ex#L24)

Redis-backed cache adapter for payment identifier idempotency.

Implements the `X402.Extensions.PaymentIdentifier.Cache` behaviour over a
[Redix](https://hexdocs.pm/redix) connection, making the replay-protection
claim safe across a clustered BEAM deployment: all nodes share one store,
so a replayed payment proof routed to two nodes is still claimed exactly
once. The atomic first-writer-wins claim (`put_new/3`) is a single
`SET key value NX PX ttl` command — Redis guarantees the
insert-if-absent-with-TTL combination atomically.

Requires the optional `redix` dependency:

    {:redix, "~> 1.5"}

## Usage

The adapter does **not** own the Redis connection — you start and
supervise Redix yourself (with your pooling, TLS, and reconnection
policy) and hand the adapter the connection's pid or registered name:

    # In your supervision tree
    children = [
      {Redix, {System.fetch_env!("REDIS_URL"), name: MyApp.Redis}},
      ...
    ]

    # Build the adapter state and configure the gate
    {:ok, cache} = X402.Extensions.PaymentIdentifier.RedisCache.new(conn: MyApp.Redis)

    plug X402.Plug.PaymentGate,
      payment_identifier_cache: {X402.Extensions.PaymentIdentifier.RedisCache, cache},
      routes: [...]

## Failure semantics

The adapter honors the `X402.Extensions.PaymentIdentifier.Cache` contract:

  * `put_new/3` returns `{:error, :already_exists}` only for a live
    (unexpired) duplicate — Redis expires entries server-side via `PX`,
    so an expired claim never blocks a retry.
  * Connection and Redis errors are returned as `{:error, reason}` (the
    `Redix.ConnectionError` / `Redix.Error` struct), which
    `X402.Plug.PaymentGate` treats as adapter failure and **fails
    closed** — a Redis outage degrades to denying new paid requests, not
    to replay.
  * Live claims are never evicted by the adapter. Configure the Redis
    server with `maxmemory-policy noeviction` so Redis doesn't drop live
    claims either; at capacity, writes then fail with an `OOM` error that
    fails closed like any other adapter error.

Entries are stored under `namespace <> payment_id` (default namespace
`"x402:payment_identifier:"`). Cached values are encoded as `"verified"`,
`"bound:" <> fingerprint`, or `"rejected:" <> Base64(term)`; rejection
reasons are decoded with
`:erlang.binary_to_term/2` in `:safe` mode, so they must be composed of
existing atoms and data terms (which is true for every reason this
library produces).

# `t`

```elixir
@type t() :: %X402.Extensions.PaymentIdentifier.RedisCache{
  command: module(),
  conn: term(),
  namespace: String.t(),
  ttl_ms: pos_integer()
}
```

Adapter state built by `new/1`.

Passed as the second element of the `{RedisCache, cache}` adapter tuple
and handed back to every callback.

# `delete`
*since 0.6.0* 

```elixir
@spec delete(t(), X402.Extensions.PaymentIdentifier.Cache.key()) ::
  X402.Extensions.PaymentIdentifier.Cache.write_result()
```

Removes the entry for a payment identifier (`DEL`), releasing its claim.

# `get`
*since 0.6.0* 

```elixir
@spec get(t(), X402.Extensions.PaymentIdentifier.Cache.key()) ::
  X402.Extensions.PaymentIdentifier.Cache.get_result()
```

Looks up a payment identifier (`GET`).

Returns `:miss` for absent or expired entries — Redis removes expired
keys server-side. A stored value that cannot be decoded is reported as
`{:error, {:invalid_cache_entry, raw}}` (fails closed at the gate).

# `new`
*since 0.6.0* 

```elixir
@spec new(keyword()) :: {:ok, t()} | {:error, :missing_dependency}
```

Builds the adapter state for a running Redix connection.

Returns `{:error, :missing_dependency}` when the optional `redix`
dependency is unavailable and no `:command` module was supplied. Raises
`NimbleOptions.ValidationError` for invalid options (programmer error).

## Options

* `:conn` (`t:term/0`) - Required. The Redix connection: a pid or the name the connection was registered
  under. The adapter never starts or supervises the connection.

* `:ttl_ms` (`t:pos_integer/0`) - Time-to-live for entries in milliseconds (Redis `PX`). The default value is `3600000`.

* `:namespace` (`t:String.t/0`) - Prefix for the Redis keys holding payment identifier claims. The default value is `"x402:payment_identifier:"`.

* `:command` - Module implementing
  `X402.Extensions.PaymentIdentifier.RedisCache.Command` used to execute
  Redis commands. Defaults to `Redix`. Injectable for testing without a
  live Redis server.

## Examples

    {:ok, cache} =
      X402.Extensions.PaymentIdentifier.RedisCache.new(
        conn: MyApp.Redis,
        ttl_ms: :timer.minutes(30),
        namespace: "myapp:x402:"
      )

# `put`
*since 0.6.0* 

```elixir
@spec put(
  t(),
  X402.Extensions.PaymentIdentifier.Cache.key(),
  X402.Extensions.PaymentIdentifier.Cache.value()
) :: X402.Extensions.PaymentIdentifier.Cache.write_result()
```

Unconditionally stores a value, resetting its TTL (`SET PX`).

# `put_new`
*since 0.6.0* 

```elixir
@spec put_new(
  t(),
  X402.Extensions.PaymentIdentifier.Cache.key(),
  X402.Extensions.PaymentIdentifier.Cache.value()
) :: X402.Extensions.PaymentIdentifier.Cache.put_new_result()
```

Atomically claims a payment identifier (`SET NX PX`).

The insert-if-absent and the TTL are one Redis command, so concurrent
claims for the same identifier — from any node — resolve to exactly one
`:ok`; every other caller gets `{:error, :already_exists}`. An expired
entry never blocks a new claim (Redis expiry is server-side), and a live
claim is never evicted by the adapter.

---

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