Packages
Elixir implementation of the Machine Payments Protocol (MPP) — HTTP 402 payment middleware for AI agents and machine-to-machine commerce. Supports Stripe, Tempo, and generic EVM payment methods with pluggable architecture.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/mpp/tempo/con_cache_store.ex
defmodule MPP.Tempo.ConCacheStore do
@moduledoc """
Built-in ETS-based dedup store using ConCache (TTL-enabled ETS wrapper by Saša Jurić).
Requires the `con_cache` optional dependency:
{:con_cache, "~> 1.1"}
## Setup
Add to your supervision tree:
children = [
MPP.Tempo.ConCacheStore.child_spec(ttl: to_timeout(minute: 10))
]
Then pass in method_config:
plug MPP.Plug,
method: MPP.Methods.Tempo,
method_config: %{
"rpc_url" => "https://rpc.moderato.tempo.xyz",
"store" => MPP.Tempo.ConCacheStore
}
If you override the cache name in your supervision tree, pass the same name in
`method_config`:
children = [
MPP.Tempo.ConCacheStore.child_spec(name: :my_custom_dedup, ttl: to_timeout(minute: 10))
]
plug MPP.Plug,
method: MPP.Methods.Tempo,
method_config: %{
"rpc_url" => "https://rpc.moderato.tempo.xyz",
"store" => {MPP.Tempo.ConCacheStore, name: :my_custom_dedup}
}
## TTL and Challenge Expiry
The store's TTL **must be ≥ your challenge `expires_in`** to prevent replay.
If the cache evicts a tx hash before the challenge expires, the same tx can
be replayed. A good default is 2× the challenge expiry.
Example: if your Plug uses `expires_in: 300` (5 min), set the store TTL to
at least `to_timeout(minute: 10)`.
## Options
* `:ttl` — time-to-live for entries in milliseconds. Default: 5 minutes.
* `:name` — registered name for the ConCache process. Default: `:mpp_tempo_dedup`.
Override to avoid child ID collisions if your app already supervises other
ConCache instances.
* `:ttl_check_interval` — how often to sweep expired entries. Default: 30 seconds.
"""
@behaviour MPP.Tempo.Store
alias MPP.Tempo.Store
@cache_name :mpp_tempo_dedup
@default_ttl_ms to_timeout(minute: 5)
@default_check_interval_ms to_timeout(second: 30)
@doc """
Returns a child spec for the ConCache process.
Start under your application's supervision tree:
children = [
MPP.Tempo.ConCacheStore.child_spec(ttl: :timer.minutes(10))
]
The child spec `id` is `{MPP.Tempo.ConCacheStore, name}` to avoid collisions
with other ConCache instances in the same supervisor.
"""
@spec child_spec(keyword()) :: Supervisor.child_spec()
def child_spec(opts \\ []) do
name = Keyword.get(opts, :name, @cache_name)
ttl = Keyword.get(opts, :ttl, @default_ttl_ms)
check_interval = Keyword.get(opts, :ttl_check_interval, @default_check_interval_ms)
[name: name, ttl_check_interval: check_interval, global_ttl: ttl]
|> ConCache.child_spec()
|> Map.put(:id, {__MODULE__, name})
end
@impl Store
def get(key), do: get(key, [])
@spec get(String.t(), keyword()) :: {:ok, term()} | :not_found
def get(key, opts) do
case ConCache.get(cache_name(opts), key) do
nil -> :not_found
value -> {:ok, value}
end
end
@impl Store
def put(key, value), do: put(key, value, [])
@spec put(String.t(), term(), keyword()) :: :ok
def put(key, value, opts) do
ConCache.put(cache_name(opts), key, value)
:ok
end
@impl Store
def check_and_mark(key, value), do: check_and_mark(key, value, [])
@spec check_and_mark(String.t(), term(), keyword()) :: :ok | {:error, :already_exists}
def check_and_mark(key, value, opts) do
cache_name = cache_name(opts)
ConCache.isolated(cache_name, key, fn ->
case ConCache.get(cache_name, key) do
nil ->
ConCache.put(cache_name, key, value)
:ok
_existing ->
{:error, :already_exists}
end
end)
end
defp cache_name(opts), do: Keyword.get(opts, :name, @cache_name)
end