An Elixir library for currency conversion at Brazil's official exchange rate. :ptax converts Money amounts between any two currencies quoted by the Brazilian Central Bank (Banco Central do Brasil, BCB), for the latest business day or for any past date, matching BCB's own online converter.
The library takes its name from those rates. PTAX is the reference rate used in financial contracts, tax reporting, and regulatory filings in Brazil: on every business day BCB publishes a bulletin on its exchange rates page, listing, per currency, a bid and an ask against BRL and against USD.
Add :ptax to your project's dependencies in mix.exs:
# mix.exs
def deps do
[
{:ptax, "~> 3.0"}
]
endConverted amounts are rounded to 7 decimal places. The rate itself is never rounded, so scaling the amount scales the result exactly.
iex> PTAX.exchange(Money.new!(:USD, "100"), :BRL)
{:ok, %Money{}}
iex> PTAX.exchange!(Money.new!(:USD, "100"), :BRL)
%Money{}The bulletin comes out at 13:00 in São Paulo. Before that time the lookup reads the previous business day's bulletin, and it falls back one more business day when the expected bulletin is not available yet. Business days skip weekends and the Brazilian national bank holidays, the same calendar that ANBIMA publishes.
iex> PTAX.exchange(Money.new!(:GBP, "50"), :BRL, ~D[2026-07-31])
{:ok, Money.new(:BRL, "341.8150000")}
iex> PTAX.exchange!(Money.new!(:GBP, "50"), :BRL, ~D[2026-07-31])
Money.new(:BRL, "341.8150000")Dates with no bulletin (weekends, holidays) return an error, or raise with the bang variants:
iex> PTAX.exchange(Money.new!(:USD, "100"), :BRL, ~D[2025-12-25])
{:error, %PTAX.QuotesNotFoundError{date: ~D[2025-12-25]}}
iex> PTAX.exchange!(Money.new!(:USD, "100"), :BRL, ~D[2025-12-25])
** (PTAX.QuotesNotFoundError) no quotes published for 2025-12-25Currencies BCB does not quote return a PTAX.CurrencyNotQuotedError:
iex> PTAX.exchange(Money.new!(:USD, "100"), :ZWL, ~D[2026-07-31])
{:error, %PTAX.CurrencyNotQuotedError{currency: :ZWL}}BCB treats a conversion between two currencies other than BRL and USD as its own operation, not as a conversion into USD followed by one out of it. Routing an amount through an intermediate currency yourself does not reproduce the published result, and the difference reaches several percent on currencies with a wide spread.
Once BCB publishes a bulletin it never changes, so each one is downloaded once and cached on disk. A bulletin that cannot be written or read is fetched from BCB instead.
:ptax also ships as an escript. Install it with mix escript.install:
$ mix escript.install hex ptaxPass the amount with its currency, as a code or a symbol. The result is in BRL unless --to names another currency, and --date picks the bulletin to use:
$ ptax 50 GBP --date 2026-07-31
R$ 341,82
$ ptax "US$ 100"
R$ 507,67
$ ptax 100 BRL --to USD
US$ 19,70PTAX reads quotes through the PTAX.Rates behaviour. Point :ptax, :rates at a stub implementing it and a test suite serves known rates instead of reaching BCB.
Any module implementing the behaviour works. With Mox:
# mix.exs
{:mox, "~> 1.2", only: :test}
# config/test.exs
config :ptax, rates: MyApp.RatesMock
# test/test_helper.exs
Mox.defmock(MyApp.RatesMock, for: PTAX.Rates)
ExUnit.start()Both callbacks receive the pair being converted and return the rate from the first currency to the second, as a Decimal: rate/3 for the given date, used by PTAX.exchange/3, and rate/2 for the latest bulletin, used by PTAX.exchange/2.
defmodule MyApp.ConversionTest do
use ExUnit.Case, async: true
test "converts at the published rate" do
Mox.stub(MyApp.RatesMock, :rate, fn
:USD, :BRL, ~D[2026-07-31] -> {:ok, Decimal.new("5.4321")}
:BRL, :USD, ~D[2026-07-31] -> {:ok, Decimal.new("0.1834")}
end)
assert PTAX.exchange!(Money.new!(:USD, "100"), :BRL, ~D[2026-07-31]) == Money.new(:BRL, "543.2100000")
assert PTAX.exchange!(Money.new!(:BRL, "100"), :USD, ~D[2026-07-31]) == Money.new(:USD, "18.3400000")
end
endex_money— theMoneytype PTAX converts