nym_node_reward_tracker
  1. Reward transaction export
  • Nym node reward tracker (docs index)
  • Common utilities
  • Cosmos Tx Parsing Helpers
  • Snapshot tracker
  • Cache: chain-native event cache for epoch-by-epoch rewards
  • Epoch-by-epoch reward replay
  • Reward transaction export
  • CLI
  • Node Interest Rates Walkthrough on Nyx (last 120 hours)
  • Notebook tests

On this page

  • Quick API proof (curl)
    • Nyx tx search API quick intro (curl)
  • Imports + constants
  • Wallet rows
    • load_wallets
    • RewardTxRow
    • RewardMessage
    • WalletRow
  • Reused common date + tx query helpers
  • Decode execute msgs + reward detection
    • extract_action
    • decode_execute_msg
  • Event interpretation (amount + node_id fallback)
    • node_id_from_tx_response_events
    • node_id_from_wasm_events
    • sum incoming NYM (shared helper)
    • tx fee in NYM + node_id fallback
    • sum_fee_nym
    • Build export rows from a tx
    • Tx URL fallback chain
    • resolve_tx_url
    • build_reward_rows_for_tx
    • Live Nyx extraction proof (real API)
  • Pricing (CoinGecko) + nearest-point selection
    • nearest_price
    • fetch_coingecko_prices_range
    • enrich rows
    • enrich_rows_with_prices
  • Output writing + CLI-friendly table log
    • write_rows
    • log_rows_table
  • Main entrypoint
    • run_reward_transactions
  • Python live end-to-end demo
  • Report an issue

Other Formats

  • CommonMark

Reward transaction export

Quick API proof (curl)

Nyx tx search API quick intro (curl)

The examples below intentionally expose the exact JSON shape this notebook needs: - txs[].body.messages[] execute message type/contract/action, - tx_responses[].logs[].events[] reward/transfer/coin_received attributes, - and pagination.next_key for paging.

NYX_TX_REST="https://api.nymtech.net/cosmos/tx/v1beta1/txs"
WALLET="n127c69pasr35p76amfczemusnutr8mtw78s8xl7"

curl -sG "$NYX_TX_REST"   --data-urlencode "query=message.sender='${WALLET}'"   --data-urlencode "pagination.limit=1"   --data-urlencode "pagination.count_total=true"   --data-urlencode "order_by=ORDER_BY_DESC" | jq '{
  pagination: .pagination,
  tx_meta: (.tx_responses[0] | {txhash, height, timestamp}),
  reward_exec_messages: (
    .txs[0].body.messages
    | to_entries
    | map(
        select((.value."@type" // "") | endswith("MsgExecuteContract"))
        | {
            msg_index: .key,
            type: .value."@type",
            contract: (.value.contract // ""),
            action_keys: (
              if (.value.msg | type) == "object" then ((.value.msg // {}) | keys)
              elif (.value.msg | type) == "string" then
                (try ((.value.msg | @base64d | fromjson) | keys)
                 catch try ((.value.msg | fromjson) | keys)
                 catch [])
              else [] end
            )
          }
      )
  ),
  relevant_events: [
    .tx_responses[0].logs[]?.events[]?
    | select((.type == "transfer") or (.type == "coin_received") or (.type | startswith("wasm")))
    | {
        type,
        msg_index: ([.attributes[]? | select(.key == "msg_index") | .value][0]),
        amount: ([.attributes[]? | select(.key == "amount") | .value][0]),
        recipient: ([.attributes[]? | select((.key == "recipient") or (.key == "receiver")) | .value][0]),
        node_id: ([.attributes[]? | select((.key == "node_id") or (.key == "mix_id")) | .value][0])
      }
  ]
}'
{
  "pagination": null,
  "tx_meta": {
    "txhash": "C2AFFF3A8A967B59EB18B4C8EDF4A180688D7EA5E8B1BE3F37916B98723DC420",
    "height": "22250074",
    "timestamp": "2026-02-04T12:18:10Z"
  },
  "reward_exec_messages": [
    {
      "msg_index": 0,
      "type": "/cosmwasm.wasm.v1.MsgExecuteContract",
      "contract": "n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr",
      "action_keys": [
        "update_cost_params"
      ]
    }
  ],
  "relevant_events": []
}
NYX_TX_REST="https://api.nymtech.net/cosmos/tx/v1beta1/txs"
WALLET="n127c69pasr35p76amfczemusnutr8mtw78s8xl7"

RESP1=$(curl -sG "$NYX_TX_REST"   --data-urlencode "query=message.sender='${WALLET}'"   --data-urlencode "pagination.limit=1"   --data-urlencode "pagination.count_total=true"   --data-urlencode "order_by=ORDER_BY_DESC")

echo "$RESP1" | jq '{page: 1, next_key: .pagination.next_key, txhash: .tx_responses[0].txhash}'

NEXT=$(echo "$RESP1" | jq -r '.pagination.next_key')
if [ "$NEXT" = "null" ] || [ -z "$NEXT" ]; then
  echo "No next_key returned (only one page of results)."
  exit 0
fi

RESP2=$(curl -sG "$NYX_TX_REST"   --data-urlencode "query=message.sender='${WALLET}'"   --data-urlencode "pagination.limit=1"   --data-urlencode "pagination.key=$NEXT"   --data-urlencode "order_by=ORDER_BY_DESC")

echo "$RESP2" | jq '{page: 2, next_key: .pagination.next_key, txhash: .tx_responses[0].txhash}'
{
  "page": 1,
  "next_key": null,
  "txhash": "C2AFFF3A8A967B59EB18B4C8EDF4A180688D7EA5E8B1BE3F37916B98723DC420"
}
No next_key returned (only one page of results).

Imports + constants

We reuse shared helpers from nym_node_reward_tracker.common: - build_session (requests.Session with headers) - request_json (GET JSON with retry/backoff + logging) - numeric parsing helpers (to_int, to_decimal)

Wallet rows

We read wallet-addresses.csv (address,tag) into small Pydantic models so validation and serialization are explicit.


load_wallets


def load_wallets(
    wallets_csv:str | Path
)->List[WalletRow]:

RewardTxRow


def RewardTxRow(
    data:Any
)->None:

Normalized reward-withdrawal transaction row before fiat enrichment/export.


RewardMessage


def RewardMessage(
    data:Any
)->None:

Parsed reward withdrawal message with resolved node_id and action kind.


WalletRow


def WalletRow(
    data:Any
)->None:

Wallet input row from CSV (address, optional tag).

from tempfile import TemporaryDirectory
with TemporaryDirectory() as td:
    p = Path(td) / "wallets.csv"
    p.write_text("address,tag\nn1abc,test\n", encoding="utf-8")
    ws = load_wallets(p)
    assert ws == [WalletRow(address="n1abc", tag="test")]

ws
[WalletRow(address='n1abc', tag='test')]

Reused common date + tx query helpers

The generic infrastructure helpers now live in 00_common.ipynb and are imported here: - parse_rfc3339_to_utc - parse_date_filters - build_tx_query - iter_txs_by_query

This keeps wallet tx pagination and date semantics identical across notebooks.

from nym_node_reward_tracker.common import _FakeResp, _FakeSession
assert parse_rfc3339_to_utc("2025-12-04T16:31:51Z").tzinfo == dt.timezone.utc

s, e = parse_date_filters("2025-01-01", "2025-01-01")
assert s.isoformat().startswith("2025-01-01T00:00:00")
assert e.isoformat().startswith("2025-01-02T00:00:00")

page1 = {
    "txs": [{"body": {"messages": []}, "auth_info": {"fee": {"amount": []}}}],
    "tx_responses": [{"txhash": "A", "timestamp": "2025-01-01T00:00:00Z"}],
    "pagination": {"next_key": "K1", "total": "2"},
}
page2 = {
    "txs": [{"body": {"messages": []}, "auth_info": {"fee": {"amount": []}}}],
    "tx_responses": [{"txhash": "B", "timestamp": "2025-01-02T00:00:00Z"}],
    "pagination": {"next_key": None, "total": "2"},
}

fake = _FakeSession([_FakeResp(page1), _FakeResp(page2)])
rows = list(iter_txs_by_query(fake, nyx_tx_rest="https://example", query="q", page_limit=1))
assert [r[1]["txhash"] for r in rows] == ["A", "B"]

assert build_tx_query("n1x", "sender") == "message.sender='n1x'"
assert build_tx_query("n1x", "recipient") == "transfer.recipient='n1x'"

Decode execute msgs + reward detection

We import shared tx parsing helpers from 00_cosmos_tx_parsing.ipynb and keep only reward-specific message decoding here for optional action-label enrichment.


extract_action


def extract_action(
    exec_msg:Dict[str, Any]
)->str:

decode_execute_msg


def decode_execute_msg(
    msg_field:Any
)->Optional[Dict[str, Any]]:
raw = {"withdraw_operator_reward": {"mix_id": 7}}
b64 = base64.b64encode(json.dumps(raw).encode("utf-8")).decode("utf-8")
assert decode_execute_msg(raw) == raw
assert decode_execute_msg(b64) == raw
assert extract_action(raw) == "withdraw_operator_reward"

Now we detect rewards from withdraw wasm events (event-first). Optional message decoding is used only to enrich the action label.

logs = {
    "logs": [{
        "msg_index": "0",
        "events": [{
            "type": "wasm-v2_withdraw_operator_reward",
            "attributes": [{"key": "_contract_address", "value": DEFAULT_MIXNET_CONTRACT}, {"key": "mix_id", "value": "2196"}],
        }],
    }]
}
by_idx = events_by_msg_index(logs)
found = _reward_withdrawals_from_events_by_index(by_idx, mixnet_contract=DEFAULT_MIXNET_CONTRACT)
assert found == [{"msg_index": 0, "action": "withdraw_operator_reward", "node_id": 2196}]

msgs = [{"@type": "/cosmwasm.wasm.v1.MsgExecuteContract", "contract": DEFAULT_MIXNET_CONTRACT, "msg": {"withdraw_operator_reward": {}}}]
assert _enrich_action_from_message(msgs, msg_index=0, mixnet_contract=DEFAULT_MIXNET_CONTRACT) == "withdraw_operator_reward"

Event interpretation (amount + node_id fallback)

To compute received NYM and node identifiers, we reuse parse_coins, events_by_msg_index, and incoming-amount helpers from 00_cosmos_tx_parsing.ipynb. Local helpers only handle reward-specific fallback semantics.


node_id_from_tx_response_events


def node_id_from_tx_response_events(
    tx_resp:Dict[str, Any]
)->Optional[int]:

node_id_from_wasm_events


def node_id_from_wasm_events(
    events:List[Dict[str, Any]]
)->Optional[int]:
assert parse_coins("100unym,5ufoo") == [(100, "unym"), (5, "ufoo")]

We group events by msg_index with shared logic from 00_cosmos_tx_parsing.ipynb.

evs = events_by_msg_index({"logs": [{"msg_index": "0", "events": [{"type": "transfer", "attributes": []}]}]})
assert 0 in evs and evs[0][0]["type"] == "transfer"
fake_resp = {"logs": [{"msg_index": "0", "events": [{"type": "transfer", "attributes": []}]}]}
evs = events_by_msg_index(fake_resp, logger=logging.getLogger("t"), txhash="x")
assert 0 in evs and evs[0][0]["type"] == "transfer"

sum incoming NYM (shared helper)

addr = "n1abc"

# transfer + coin_received mirror the same movement -> count once
events = [
    {"type": "transfer", "attributes": [{"key": "recipient", "value": addr}, {"key": "amount", "value": "2000000unym"}]},
    {"type": "coin_received", "attributes": [{"key": "receiver", "value": addr}, {"key": "amount", "value": "2000000unym"}]},
]
assert sum_incoming_nym_from_events(events, addr) == Decimal("2")

# fallback: if transfer is absent, use coin_received
events_coin_only = [
    {"type": "coin_received", "attributes": [{"key": "receiver", "value": addr}, {"key": "amount", "value": "1000000unym"}]},
]
assert sum_incoming_nym_from_events(events_coin_only, addr) == Decimal("1")
addr = "n1abc"

# transfer + coin_received mirror the same movement -> count once
events = [
    {"type": "transfer", "attributes": [{"key": "recipient", "value": addr}, {"key": "amount", "value": "2000000unym"}]},
    {"type": "coin_received", "attributes": [{"key": "receiver", "value": addr}, {"key": "amount", "value": "2000000unym"}]},
]
assert sum_incoming_nym_from_events(events, addr) == Decimal("2")

# fallback: if transfer is absent, use coin_received
events_coin_only = [
    {"type": "coin_received", "attributes": [{"key": "receiver", "value": addr}, {"key": "amount", "value": "1000000unym"}]},
]
assert sum_incoming_nym_from_events(events_coin_only, addr) == Decimal("1")

tx fee in NYM + node_id fallback


sum_fee_nym


def sum_fee_nym(
    tx:Dict[str, Any]
)->Decimal:
tx = {"auth_info": {"fee": {"amount": [{"denom": "unym", "amount": "3000"}]}}}
assert sum_fee_nym(tx) == Decimal("0.003")

wasm_events = [{"type": "wasm-v2_withdraw_operator_reward", "attributes": [{"key": "mix_id", "value": "2196"}]}]
assert node_id_from_wasm_events(wasm_events) == 2196

tx_resp = {"events": [{"type": "wasm-v2_withdraw_operator_reward", "attributes": [{"key": "mix_id", "value": "2196"}]}]}
assert node_id_from_tx_response_events(tx_resp) == 2196

Build export rows from a tx

For each reward message we create one row.

If a tx contains multiple reward messages, we allocate the tx fee evenly across them.

Tx URL fallback chain

We resolve a robust tx_url per transaction with this chain: primary tx base (default Nyx tx REST), then explicit tx API base, then RPC /tx query.


resolve_tx_url


def resolve_tx_url(
    txhash:str, session:Any | None, explorer_base:str, tx_api_base:str, tx_rpc_base:str, timeout:int,
    logger:logging.Logger
)->str:
from nym_node_reward_tracker.common import _FakeResp, _FakeSession
sess = _FakeSession([
    _FakeResp({}, status_code=404),
    _FakeResp({"tx_response": {"txhash": "H"}}, status_code=200),
])
url = resolve_tx_url(
    "H",
    session=sess,
    explorer_base="https://example.invalid/nyx/tx",
    tx_api_base="https://api.nymtech.net/cosmos/tx/v1beta1/txs",
    tx_rpc_base="https://rpc.nymtech.net",
    timeout=5,
    logger=logging.getLogger("t"),
)
assert url.endswith('/cosmos/tx/v1beta1/txs/H')

build_reward_rows_for_tx


def build_reward_rows_for_tx(
    wallet:WalletRow, tx:Dict[str, Any], tx_resp:Dict[str, Any], mixnet_contract:str, explorer_base:str,
    tx_api_base:str, tx_rpc_base:str, logger:logging.Logger, session:Any | None=None
)->List[Dict[str, Any]]:
w = WalletRow(address="n1abc", tag="t")
tx = {
    "body": {
        "messages": [
            {
                "@type": "/cosmwasm.wasm.v1.MsgExecuteContract",
                "contract": DEFAULT_MIXNET_CONTRACT,
                "msg": {"withdraw_operator_reward": {}},
            }
        ]
    },
    "auth_info": {"fee": {"amount": [{"denom": "unym", "amount": "1000"}]}}
}

tx_resp = {
    "txhash": "H",
    "height": "1",
    "timestamp": "2025-01-01T00:00:00Z",
    "logs": [{
        "msg_index": "0",
        "events": [
            {"type": "wasm-v2_withdraw_operator_reward", "attributes": [{"key": "_contract_address", "value": DEFAULT_MIXNET_CONTRACT}, {"key": "mix_id", "value": "2196"}]},
            {"type": "transfer", "attributes": [{"key": "recipient", "value": "n1abc"}, {"key": "amount", "value": "1000000unym"}]},
        ],
    }],
}

rows = build_reward_rows_for_tx(
    wallet=w,
    tx=tx,
    tx_resp=tx_resp,
    mixnet_contract=DEFAULT_MIXNET_CONTRACT,
    explorer_base=DEFAULT_NYX_TX_REST,
    tx_api_base=DEFAULT_TX_API_BASE,
    tx_rpc_base=DEFAULT_TX_RPC_BASE,
    logger=logging.getLogger("t"),
)
assert rows and rows[0]["node_id"] == 2196
assert rows[0]["amount_nym"] == "1.000000"
assert rows[0]["action"] == "withdraw_operator_reward"

Live Nyx extraction proof (real API)

wallet = WalletRow(address="n127c69pasr35p76amfczemusnutr8mtw78s8xl7", tag="operator")
session = build_session("nym-node-reward-tracker/reward-transactions-live-demo")
query = build_tx_query(wallet.address, "sender")

l = list(iter_txs_by_query(session, nyx_tx_rest=DEFAULT_NYX_TX_REST, query=query, page_limit=20, order_by="ORDER_BY_DESC", progress_desc=None))
print(len(l))
14
logger = logging.getLogger("live_reward_demo")
for i in range(len(l)):
    tx, tx_resp = l[i]
    ev_by_idx = events_by_msg_index(tx_resp, logger=logger, txhash=str(tx_resp.get("txhash") or ""))
    reward_msgs = _reward_withdrawals_from_events_by_index(ev_by_idx, mixnet_contract=DEFAULT_MIXNET_CONTRACT)
    if reward_msgs:
        print(i, tx_resp.get("txhash"), reward_msgs)
        break
12 30C980A159389BC26BA2B199CD13EEF4F269DF63D82F588AA67C2882EFEF295C [{'msg_index': 0, 'action': 'withdraw_operator_reward', 'node_id': 2196}]
idx = reward_msgs[0]["msg_index"]
evs = ev_by_idx[idx]
ev_types = [str(e.get("type") or "") for e in evs]
print(ev_types)
['message', 'execute', 'wasm-v2_withdraw_operator_reward', 'coin_spent', 'coin_received', 'transfer']
amount_nym = sum_incoming_nym_from_events(evs, wallet.address)
print(amount_nym)
3009.552561
wallet = WalletRow(address="n127c69pasr35p76amfczemusnutr8mtw78s8xl7", tag="operator")
session = build_session("nym-node-reward-tracker/reward-transactions-live-demo")
query = build_tx_query(wallet.address, "sender")

scanned = 0
live_rows: List[Dict[str, Any]] = []
for tx, resp in iter_txs_by_query(
    session,
    nyx_tx_rest=DEFAULT_NYX_TX_REST,
    query=query,
    page_limit=20,
    order_by="ORDER_BY_DESC",
    progress_desc=None,
):
    scanned += 1
    live_rows.extend(
        build_reward_rows_for_tx(
            wallet=wallet,
            tx=tx,
            tx_resp=resp,
            mixnet_contract=DEFAULT_MIXNET_CONTRACT,
            explorer_base=DEFAULT_NYX_TX_REST,
            tx_api_base=DEFAULT_TX_API_BASE,
            tx_rpc_base=DEFAULT_TX_RPC_BASE,
            logger=logging.getLogger("live_reward_demo"),
            session=session,
        )
    )
    if len(live_rows) >= 3 or scanned >= 200:
        break

print({"scanned_txs": scanned, "reward_rows_found": len(live_rows)})
for row in live_rows[:3]:
    print({
        "txhash": row["txhash"],
        "msg_index": row["msg_index"],
        "action": row["action"],
        "node_id": row["node_id"],
        "amount_nym": row["amount_nym"],
        "tx_fee_nym": row["tx_fee_nym"],
    })
live_rows
{'scanned_txs': 14, 'reward_rows_found': 1}
{'txhash': '30C980A159389BC26BA2B199CD13EEF4F269DF63D82F588AA67C2882EFEF295C', 'msg_index': 0, 'action': 'withdraw_operator_reward', 'node_id': 2196, 'amount_nym': '3009.552561', 'tx_fee_nym': '0.005163'}
[{'timestamp_utc': '2025-12-04T16:31:51+00:00',
  'wallet': 'n127c69pasr35p76amfczemusnutr8mtw78s8xl7',
  'tag': 'operator',
  'txhash': '30C980A159389BC26BA2B199CD13EEF4F269DF63D82F588AA67C2882EFEF295C',
  'tx_url': 'https://api.nymtech.net/cosmos/tx/v1beta1/txs/30C980A159389BC26BA2B199CD13EEF4F269DF63D82F588AA67C2882EFEF295C',
  'height': '21312646',
  'msg_index': 0,
  'action': 'withdraw_operator_reward',
  'node_id': 2196,
  'amount_nym': '3009.552561',
  'tx_fee_nym': '0.005163',
  'fee_alloc_nym': '0.005163',
  'net_nym': '3009.547398',
  'price_timestamp_utc': '',
  'price_time_diff_seconds': ''}]

Pricing (CoinGecko) + nearest-point selection

We fetch a price series over the minimal time window that covers all receipts, then select the nearest price point per tx timestamp.

Note: for interface compatibility we keep the cache_dir argument in run_reward_transactions, but this rewrite intentionally does not cache price responses.


nearest_price


def nearest_price(
    points:List[Tuple[int, Decimal]], ts:int
)->Tuple[Optional[int], Optional[Decimal], Optional[int]]:

fetch_coingecko_prices_range


def fetch_coingecko_prices_range(
    session:Any, coingecko_base:str, coin_id:str, vs_currency:str, from_ts:int, to_ts:int, api_key:Optional[str],
    timeout:int=60, retries:int=6, logger:Optional[logging.Logger]=None
)->List[Tuple[int, Decimal]]:
assert nearest_price([(100, Decimal("1.0")), (200, Decimal("2.0"))], 180)[1] == Decimal("2.0")

# Fake CoinGecko response
from nym_node_reward_tracker.common import _FakeResp, _FakeSession
fake = _FakeSession([_FakeResp({"prices": [[1000, 0.5]]})])
pts = fetch_coingecko_prices_range(
    fake,
    coingecko_base="https://example",
    coin_id="nym",
    vs_currency="eur",
    from_ts=0,
    to_ts=2,
    api_key=None,
)
assert pts == [(1, Decimal("0.5"))]

enrich rows


enrich_rows_with_prices


def enrich_rows_with_prices(
    rows:List[Dict[str, Any]], session:Any, currency:str, coingecko_base:str, coin_id:str, api_key:Optional[str],
    logger:logging.Logger
)->None:
rows = [{
    "timestamp_utc": "2025-01-01T00:00:00+00:00",
    "amount_nym": "1.000000",
}]
fake = _FakeSession([_FakeResp({"prices": [[1735689600000, 0.5]]})])  # 2025-01-01T00:00:00Z
enrich_rows_with_prices(
    rows,
    session=fake,
    currency="eur",
    coingecko_base="https://example",
    coin_id="nym",
    api_key=None,
    logger=logging.getLogger("t"),
)
assert rows[0]["price_eur_per_nym"] != ""
assert rows[0]["value_eur"] == "0.50"

Output writing + CLI-friendly table log

  • We log a compact table (CLI visibility).
  • We write .csv or .xlsx based on output filename suffix.

write_rows


def write_rows(
    rows:List[Dict[str, Any]], currency:str, out_path:str | Path
)->None:

log_rows_table


def log_rows_table(
    rows:List[Dict[str, Any]], currency:str, logger:logging.Logger
)->None:
from tempfile import TemporaryDirectory
with TemporaryDirectory() as td:
    p = Path(td) / "out.csv"
    write_rows([{"timestamp_utc": "x", "wallet": "w"}], currency="eur", out_path=p)
    assert p.exists()

Main entrypoint

This is the CLI-facing function imported by 10_cli.md.

  • return 2 when wallets CSV is empty / has no wallets,
  • return 0 when no reward receipts are found (and log a warning),
  • always query Nyx directly (no SQLite cache involvement).
  • resolve tx_url via fallback chain: explorer -> tx API -> tx RPC.

run_reward_transactions


def run_reward_transactions(
    wallets_csv:str='wallet-addresses.csv', out_csv:str='nym_rewards_tax_export.csv', currency:str='eur',
    start:Optional[str]=None, end:Optional[str]=None, nyx_api_base:Optional[str]=None,
    mixnet_contract:Optional[str]=None, coin_id:Optional[str]=None, coingecko_base:Optional[str]=None,
    coingecko_api_key:Optional[str]=None, cache_dir:Optional[str]='.cache', search_mode:str='sender',
    explorer_base:Optional[str]=None, tx_api_base:Optional[str]=None, tx_rpc_base:Optional[str]=None,
    log_file:str='nym_tax_rewards_export.log', log_level:str='INFO'
)->int:
from tempfile import TemporaryDirectory
with TemporaryDirectory() as td:
    p = Path(td) / "wallets.csv"
    p.write_text("address,tag\n", encoding="utf-8")
    rc = run_reward_transactions(wallets_csv=str(p), out_csv=str(Path(td) / "out.csv"))
    assert rc == 2
2026-02-12T13:37:07 | ERROR | nym_tax_export | No wallets found in /tmp/tmpwwvd5zuy/wallets.csv

Python live end-to-end demo

import os
from tempfile import TemporaryDirectory
with TemporaryDirectory() as td:
    wallets = Path(td) / "wallets.csv"
    wallets.write_text("address,tag\nn127c69pasr35p76amfczemusnutr8mtw78s8xl7,operator\n", encoding="utf-8")
    out = Path(td) / "rewards.csv"

    rc = run_reward_transactions(
        wallets_csv=str(wallets),
        out_csv=str(out),
        currency="eur",
        start="2025-01-01",
        end="2025-12-31",
        search_mode="sender",
        tx_api_base="https://api.nymtech.net/cosmos/tx/v1beta1/txs",
        tx_rpc_base="https://rpc.nymtech.net",
        log_file=None,
        log_level="INFO",
    )
    print("rc=", rc)
    print("out exists:", out.exists(), out)
Wallets:   0%|                                                                                                                                                                                                                  | 0/1 [00:00<?, ?wallet/s]2026-02-12T13:37:07 | INFO | nym_tax_export | Scanning wallet=n127c69pasr35p76amfczemusnutr8mtw78s8xl7 tag=operator query="message.sender='n127c69pasr35p76amfczemusnutr8mtw78s8xl7'"

Txs n127c69p: 0tx [00:00, ?tx/s]Txs n127c69p: 14tx [00:00, 1086.08tx/s]
Wallets: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00,  7.45wallet/s]Wallets: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00,  7.42wallet/s]
2026-02-12T13:37:07 | INFO | nym_tax_export | Collected 1 reward rows; fetching prices (eur)
2026-02-12T13:37:07 | INFO | nym_tax_export | CoinGecko price window unix=1764862311..1764869511 (eur)
2026-02-12T13:37:07 | INFO | nym_tax_export | Loaded 2 price points
2026-02-12T13:37:07 | INFO | nym_tax_export | 
2026-02-12T13:37:07 | INFO | nym_tax_export | Reward transactions (table)
2026-02-12T13:37:07 | INFO | nym_tax_export | 
|             timestamp_utc |                                   wallet |                   action |   node_id |   amount_nym |     net_nym |   price_eur_per_nym |   value_eur |
|---------------------------|------------------------------------------|--------------------------|-----------|--------------|-------------|---------------------|-------------|
| 2025-12-04T16:31:51+00:00 | n127c69pasr35p76amfczemusnutr8mtw78s8xl7 | withdraw_operator_reward |      2196 |  3009.552561 | 3009.547398 |          0.04261800 |      128.26 |
2026-02-12T13:37:07 | INFO | nym_tax_export | Wrote 1 rows to: /tmp/tmpomn4uzy6/rewards.csv
rc= 0
out exists: True /tmp/tmpomn4uzy6/rewards.csv
  • Report an issue