Settlement Service
The Settlement Service (apps/settlement_service) serves as both the API gateway for external clients and the settlement/netting engine for PIX transactions. It runs on port 4003 and manages the financial reconciliation between the institution and BACEN.
Responsibilities
- API gateway with authentication and rate limiting
- Request routing to DICT and SPI services
- Multilateral netting calculations
- Settlement cycle management
- STR (Sistema de Transferencia de Reservas) integration
- Reconciliation and reporting
- Liquidity monitoring and alerts
API Gateway
The Settlement Service acts as the single entry point for external API consumers, including the Admin and User portals.
Request Routing
defmodule SettlementService.Router do
use Phoenix.Router
pipeline :api do
plug :accepts, ["json"]
plug SettlementService.Plugs.RateLimiter
plug SettlementService.Plugs.RequestLogger
end
pipeline :authenticated do
plug SettlementService.Plugs.Authenticate
plug SettlementService.Plugs.Authorize
end
scope "/api/v1", SettlementService do
pipe_through [:api, :authenticated]
# Key management (proxied to DICT Service)
resources "/keys", KeyController, only: [:index, :show, :create, :delete]
post "/keys/lookup", KeyController, :lookup
resources "/claims", ClaimController, only: [:index, :show, :create, :update]
# Payments (proxied to SPI Service)
resources "/payments", PaymentController, only: [:index, :show, :create]
post "/payments/:id/return", PaymentController, :return
resources "/qrcodes", QrCodeController, only: [:create, :show]
# Settlement (handled locally)
resources "/settlements", SettlementController, only: [:index, :show]
get "/position", PositionController, :show
get "/reconciliation", ReconciliationController, :index
# Admin
get "/dashboard/stats", DashboardController, :stats
end
scope "/health" do
get "/", HealthController, :check
get "/ready", HealthController, :ready
end
endRate Limiting
The gateway implements sliding window rate limiting per client and per endpoint:
defmodule SettlementService.Plugs.RateLimiter do
import Plug.Conn
@default_limit 1000 # requests per window
@window_ms 60_000 # 1-minute window
@endpoint_limits %{
"POST /api/v1/payments" => {100, 60_000},
"POST /api/v1/keys" => {50, 60_000},
"GET /api/v1/keys/lookup" => {500, 60_000}
}
def call(conn, _opts) do
client_id = get_client_id(conn)
endpoint = "#{conn.method} #{conn.request_path}"
{limit, window} = Map.get(@endpoint_limits, endpoint, {@default_limit, @window_ms})
case check_rate(client_id, endpoint, limit, window) do
{:ok, remaining} ->
conn
|> put_resp_header("x-ratelimit-limit", to_string(limit))
|> put_resp_header("x-ratelimit-remaining", to_string(remaining))
{:error, :rate_limited, retry_after} ->
conn
|> put_resp_header("retry-after", to_string(retry_after))
|> send_resp(429, Jason.encode!(%{error: "rate_limited"}))
|> halt()
end
end
endAuthentication
The gateway validates JWT tokens issued during login:
defmodule SettlementService.Plugs.Authenticate do
def call(conn, _opts) do
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
{:ok, claims} <- Shared.Auth.verify_token(token),
{:ok, user} <- Shared.Auth.get_user(claims["sub"]) do
assign(conn, :current_user, user)
else
_ ->
conn
|> send_resp(401, Jason.encode!(%{error: "unauthorized"}))
|> halt()
end
end
endSettlement and Netting
Settlement Cycles
PIX settlement occurs in multiple daily cycles defined by BACEN. The service manages the full lifecycle:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Accumulate │────>│ Calculate │────>│ Submit │
│ Transactions│ │ Netting │ │ to BACEN │
└─────────────┘ └──────────────┘ └─────────────┘
│ │ │
│ ┌────▼─────┐ ┌────▼────┐
│ │ Net Debit│ │Confirmed│
│ │or Credit │ │ by STR │
│ └──────────┘ └─────────┘Settlement Windows
| Window | Opening | Closing | Description |
|---|---|---|---|
| Window 1 | 00:00 | 07:00 | Overnight transactions |
| Window 2 | 07:00 | 12:00 | Morning transactions |
| Window 3 | 12:00 | 17:00 | Afternoon transactions |
| Window 4 | 17:00 | 21:00 | Evening transactions |
| Window 5 | 21:00 | 00:00 | Night transactions |
INFO
Settlement windows may vary. BACEN publishes the official schedule and can adjust windows during holidays or special circumstances.
Multilateral Netting
The netting engine calculates the net position for each settlement window:
defmodule SettlementService.NettingEngine do
alias Shared.Repo
alias Shared.Schema.Transaction
def calculate_netting(window_id) do
transactions = fetch_window_transactions(window_id)
# Calculate gross positions
gross_credit = sum_credits(transactions)
gross_debit = sum_debits(transactions)
# Net position
net_position = gross_credit - gross_debit
settlement = %{
window_id: window_id,
gross_credit: gross_credit,
gross_debit: gross_debit,
net_position: net_position,
transaction_count: length(transactions),
direction: if(net_position >= 0, do: :credit, else: :debit),
calculated_at: DateTime.utc_now()
}
{:ok, _} = Repo.insert(%Shared.Schema.Settlement{} |> Ecto.Changeset.change(settlement))
{:ok, settlement}
end
defp sum_credits(transactions) do
transactions
|> Enum.filter(&(&1.direction == :inbound))
|> Enum.reduce(Decimal.new(0), &Decimal.add(&2, &1.amount))
end
defp sum_debits(transactions) do
transactions
|> Enum.filter(&(&1.direction == :outbound))
|> Enum.reduce(Decimal.new(0), &Decimal.add(&2, &1.amount))
end
endSTR Integration
The net settlement amount is transferred via STR (Sistema de Transferencia de Reservas):
defmodule SettlementService.StrClient do
def submit_settlement(settlement) do
message = build_str_message(settlement)
signed = Shared.Bacen.JwsSigner.sign(message, private_key())
case Shared.Bacen.Client.post("/str/settlement", signed) do
{:ok, %{"status" => "CONFIRMED"} = response} ->
{:ok, response}
{:ok, %{"status" => "REJECTED", "reason" => reason}} ->
{:error, {:rejected, reason}}
{:error, reason} ->
{:error, reason}
end
end
endReconciliation
The reconciliation module compares internal transaction records against BACEN statements:
defmodule SettlementService.Reconciliation do
def reconcile(date) do
internal = fetch_internal_transactions(date)
bacen_statement = fetch_bacen_statement(date)
# Match transactions by end-to-end ID
{matched, unmatched_internal, unmatched_bacen} =
match_transactions(internal, bacen_statement)
# Check for amount discrepancies in matched records
discrepancies =
matched
|> Enum.filter(fn {int, ext} -> int.amount != ext.amount end)
report = %{
date: date,
total_internal: length(internal),
total_bacen: length(bacen_statement),
matched: length(matched),
unmatched_internal: length(unmatched_internal),
unmatched_bacen: length(unmatched_bacen),
discrepancies: length(discrepancies),
status: determine_status(unmatched_internal, unmatched_bacen, discrepancies)
}
{:ok, report}
end
endLiquidity Monitoring
The service continuously monitors the institution's liquidity position:
defmodule SettlementService.LiquidityMonitor do
use GenServer
@check_interval :timer.seconds(30)
@warning_threshold 0.3 # 30% of limit
@critical_threshold 0.1 # 10% of limit
def handle_info(:check, state) do
position = SpiService.PositionManager.get_position()
ratio = position.available / position.limit
cond do
ratio <= @critical_threshold ->
alert(:critical, position)
ratio <= @warning_threshold ->
alert(:warning, position)
true ->
:ok
end
schedule_check()
{:noreply, %{state | last_check: DateTime.utc_now()}}
end
defp alert(level, position) do
Shared.Nats.publish("pix.alerts.liquidity", %{
level: level,
available: position.available,
limit: position.limit,
ratio: position.available / position.limit,
timestamp: DateTime.utc_now()
})
end
endDashboard Statistics
The service aggregates statistics for the Admin Portal dashboard:
defmodule SettlementService.Stats do
def dashboard_stats(period \\ :today) do
%{
transactions: %{
total: count_transactions(period),
successful: count_by_status(period, :confirmed),
failed: count_by_status(period, :rejected),
returned: count_by_status(period, :returned)
},
volume: %{
total: sum_amount(period),
average: avg_amount(period),
peak_tps: peak_tps(period)
},
keys: %{
total_active: count_active_keys(),
registered_today: count_keys_registered(period),
removed_today: count_keys_removed(period)
},
settlement: %{
last_window: last_settlement_summary(),
net_position: current_net_position()
}
}
end
endTelemetry
| Metric | Type | Description |
|---|---|---|
settlement.netting.calculated | Counter | Netting cycles completed |
settlement.net_position | Gauge | Current net position |
settlement.reconciliation.status | Gauge | Last reconciliation result |
gateway.request.count | Counter | API requests by endpoint |
gateway.request.duration | Histogram | API response latency |
gateway.rate_limit.rejected | Counter | Rate-limited requests |
gateway.auth.failed | Counter | Failed authentication attempts |
liquidity.ratio | Gauge | Available/limit ratio |