Skip to content

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

elixir
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
end

Rate Limiting

The gateway implements sliding window rate limiting per client and per endpoint:

elixir
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
end

Authentication

The gateway validates JWT tokens issued during login:

elixir
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
end

Settlement 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

WindowOpeningClosingDescription
Window 100:0007:00Overnight transactions
Window 207:0012:00Morning transactions
Window 312:0017:00Afternoon transactions
Window 417:0021:00Evening transactions
Window 521:0000:00Night 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:

elixir
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
end

STR Integration

The net settlement amount is transferred via STR (Sistema de Transferencia de Reservas):

elixir
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
end

Reconciliation

The reconciliation module compares internal transaction records against BACEN statements:

elixir
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
end

Liquidity Monitoring

The service continuously monitors the institution's liquidity position:

elixir
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
end

Dashboard Statistics

The service aggregates statistics for the Admin Portal dashboard:

elixir
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
end

Telemetry

MetricTypeDescription
settlement.netting.calculatedCounterNetting cycles completed
settlement.net_positionGaugeCurrent net position
settlement.reconciliation.statusGaugeLast reconciliation result
gateway.request.countCounterAPI requests by endpoint
gateway.request.durationHistogramAPI response latency
gateway.rate_limit.rejectedCounterRate-limited requests
gateway.auth.failedCounterFailed authentication attempts
liquidity.ratioGaugeAvailable/limit ratio

FluxiQ PIX - Plataforma Brasileira de Pagamento Instantaneo