ETS-backed post-settlement P&L ledger: one entry per completed cross-chain fill, recording the fee collected (in the fee token) against the native gas the fill burned, so a corridor's margin and a chain's native-token drain can be measured after the fact.
This exists because a small cross-chain fill can quote as solvable yet lose money
once destination gas lands -- e.g. a $1 fill to Ethereum L1 pays far more in ETH
gas than it earns in USDC spread, and the loss is denominated in ETH (a one-way
drain), not USDC. can_solve catches this only at quote time; this ledger
quantifies it after settlement and feeds Raxol.Payments.RebalanceAdvisor.
Mirrors Raxol.Payments.Ledger (BaseManager + ETS, caller-started). One
deliberate difference: the table is a :set keyed by intent_id (Ledger uses a
:duplicate_bag keyed by agent_id) so :ets.insert_new/2 gives O(1)
idempotency -- a re-poll of the same settlement must not double-record. Do not
"align" this back to a :duplicate_bag.
There is no price oracle in raxol_payments, so gas (native units) and fee (fee
token) are stored raw; USD margin is computed only in the aggregations, and only
when an injectable :price_fn (and :usdc_price) is supplied. Without a price,
aggregations still return raw fee_by_currency / gas_by_chain totals and a
usd_margin of nil.
Usage
{:ok, ledger} = SettlementLedger.start_link(name: :my_settlements)
{:ok, :recorded} =
SettlementLedger.record_settlement(ledger, %{
intent_id: "xi_abc",
from_chain_id: 8453,
to_chain_id: 1,
token_symbol: "USDC",
fee_collected: "2205",
fee_currency: "USDC",
fee_decimals: 6,
gas_native: 107_675_364_531_212,
gas_chain_id: 1,
gas_symbol: "ETH",
gas_status: :confirmed,
tx_hash: "0x...",
settlement_type: :public
})
SettlementLedger.report(ledger, price_fn: fn "ETH" -> Decimal.new("1700") end)
Summary
Functions
Backfill gas_native for an intent whose receipt mined after the entry was
recorded (the :pending path). Fills only when the stored gas_native is nil;
otherwise a no-op. opts may carry :gas_status (default :confirmed).
Returns a specification to start this module under a supervisor.
One aggregate over all (filtered) settlements. A negative usd_margin is the
cumulative subsidy the solver has spent keeping those corridors live.
Fetch one settlement by intent id.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_info/2.
List settlements, newest last. opts filters: :from_chain_id, :to_chain_id,
:settlement_type, :since_ms.
Margin aggregate per {from_chain_id, to_chain_id} corridor. Pricing opts
(:price_fn, :usdc_price) flow into USD fields; filter opts flow to
list_settlements/2.
Margin aggregate per destination chain (to_chain_id).
Total native-token drain (wei) per chain the solver filled on.
Record a completed fill. Idempotent by intent_id: a second record for the same
intent returns {:ok, :duplicate} and does not overwrite or re-emit telemetry.
A structured margin report: corridors, destinations, native drain, and totals.
Types
@type aggregate() :: %{ count: non_neg_integer(), gas_unknown_count: non_neg_integer(), fee_by_currency: %{required(String.t()) => Decimal.t()}, gas_by_chain: %{required(pos_integer()) => Decimal.t()}, usd_revenue: Decimal.t() | nil, usd_fee: Decimal.t() | nil, usd_gas: Decimal.t() | nil, usd_margin: Decimal.t() | nil }
Aggregated margin over a set of entries.
@type entry() :: %{ intent_id: String.t(), from_chain_id: pos_integer() | nil, to_chain_id: pos_integer() | nil, token_symbol: String.t() | nil, token_address: String.t() | nil, fee_collected: Decimal.t(), fee_currency: String.t(), fee_decimals: pos_integer(), from_amount: Decimal.t() | nil, from_symbol: String.t() | nil, from_decimals: pos_integer() | nil, to_amount: Decimal.t() | nil, to_symbol: String.t() | nil, to_decimals: pos_integer() | nil, gas_native: Decimal.t() | nil, gas_chain_id: pos_integer() | nil, gas_symbol: String.t() | nil, gas_decimals: pos_integer(), gas_status: gas_status(), estimated_gas_cost: String.t() | nil, tx_hash: String.t() | nil, settlement_type: atom() | nil, timestamp_ms: integer(), usd_margin: Decimal.t() | nil, metadata: map() }
@type gas_status() :: :confirmed | :pending | :no_public_tx | :error
Functions
@spec amend_gas(GenServer.server(), String.t(), integer() | Decimal.t(), keyword()) :: {:ok, :amended | :noop} | :error
Backfill gas_native for an intent whose receipt mined after the entry was
recorded (the :pending path). Fills only when the stored gas_native is nil;
otherwise a no-op. opts may carry :gas_status (default :confirmed).
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec cumulative_subsidy( GenServer.server(), keyword() ) :: aggregate()
One aggregate over all (filtered) settlements. A negative usd_margin is the
cumulative subsidy the solver has spent keeping those corridors live.
@spec get_settlement(GenServer.server(), String.t()) :: {:ok, entry()} | :error
Fetch one settlement by intent id.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_info/2.
@spec list_settlements( GenServer.server(), keyword() ) :: [entry()]
List settlements, newest last. opts filters: :from_chain_id, :to_chain_id,
:settlement_type, :since_ms.
@spec margin_by_corridor( GenServer.server(), keyword() ) :: %{required({pos_integer(), pos_integer()}) => aggregate()}
Margin aggregate per {from_chain_id, to_chain_id} corridor. Pricing opts
(:price_fn, :usdc_price) flow into USD fields; filter opts flow to
list_settlements/2.
@spec margin_by_destination( GenServer.server(), keyword() ) :: %{required(pos_integer()) => aggregate()}
Margin aggregate per destination chain (to_chain_id).
@spec native_drain_by_chain( GenServer.server(), keyword() ) :: %{required(pos_integer()) => Decimal.t()}
Total native-token drain (wei) per chain the solver filled on.
@spec record_settlement(GenServer.server(), map()) :: {:ok, :recorded | :duplicate}
Record a completed fill. Idempotent by intent_id: a second record for the same
intent returns {:ok, :duplicate} and does not overwrite or re-emit telemetry.
@spec report( GenServer.server(), keyword() ) :: %{corridors: map(), destinations: map(), drain: map(), totals: aggregate()}
A structured margin report: corridors, destinations, native drain, and totals.