X402.Extensions.PaymentIdentifier.RedisCache (X402 v0.6.0)

Copy Markdown View Source

Redis-backed cache adapter for payment identifier idempotency.

Implements the X402.Extensions.PaymentIdentifier.Cache behaviour over a 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" 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).

Summary

Types

t()

Adapter state built by new/1.

Functions

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

Looks up a payment identifier (GET).

Builds the adapter state for a running Redix connection.

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

Atomically claims a payment identifier (SET NX PX).

Types

t()

@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.

Functions

delete(cache, payment_id)

(since 0.6.0)

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

get(cache, payment_id)

(since 0.6.0)

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(opts)

(since 0.6.0)
@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 (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 (pos_integer/0) - Time-to-live for entries in milliseconds (Redis PX). The default value is 3600000.

  • :namespace (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(cache, payment_id, value)

(since 0.6.0)

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

put_new(cache, payment_id, value)

(since 0.6.0)

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.