BryanLabsBryanLabs
Tax

Pricing Methodology

How every USD value in a Cosmos Tax report is priced, published so it's reasonable and consistently applied.

Version v18Effective 2026-11-09

Price sources

  • wasm-indexer oracle /price endpoint (chain-registry + scraped IBC/CosmWasm-token price feeds)
  • CoinGecko daily USD price (00:00 UTC), the oracle's primary source; it reaches back to 2025-06-26
  • Coinbase Exchange daily candle open (00:00 UTC) for ATOM (ATOM-USD, from 2021-01-01) and USDT (USDT-USD, from 2021-05-04) on days CoinGecko cannot serve; over 289 overlapping ATOM days the median difference from CoinGecko was 0.08%. Other assets have no direct USD market there and stay unpriced before CoinGecko's first day

Timestamp granularity

daily (UTC calendar day); every event on a given day for a given denom uses that day's single reference price, not an intraday price. The price is for that exact day: a missing day is never filled from an earlier one, except today before the oracle's first refresh of the day, which uses yesterday's price

Aggregation rule

one reference USD price per (denom, UTC day), read once from the oracle and cached; every row for that denom/day reuses the same value

Fallback order

  1. asset review first: suspected-spam receipts remain unreviewed until the user explicitly excludes them or supplies token identity, quantity, value and basis; no default exclusion or override
  2. for an explicit override, use the user's exact total USD value and basis with their chosen token and acquisition date, not a guessed native ATOM price
  3. wasm-indexer oracle price for the exact denom and day
  4. documented $1 USD peg for exact allowlisted USDC denominations when the oracle has no price
  5. no synthetic fallback for any other asset: the row is flagged price_missing rather than valued at $0

When a price is missing

rows without a price are flagged price_missing and excluded from USD totals rather than counted as a real zero; report and export summaries surface a count so a gap is never silently understated. A Form 8949 line whose sale date has no price says so in its description and in the Proceeds unknown column (X-Proceeds-Unknown-Count, Schedule D unknown_proceeds_lines) instead of showing a confirmed 0.00, and an acquisition with no price at receipt is basis unknown

IBC tokens

A token that came to the Hub over IBC is priced as the asset it is on its home chain. When the Hub's own chain-registry asset list has no price for it, its denom trace is followed one transfer hop at a time back to the chain that issued it, and every hop has to be proven: either the registry lists that exact channel pair with a live client for the expected chain, or the peer chain's own node shows the channel pointing back on a client of the Hub. Every node consulted must report the chain id it is expected to have. The symbol, decimals and CoinGecko id then come from the home chain's registry asset list, never from a symbol that merely matches. No token is given its underlying asset's price.

Some IBC tokens stay price_missing on purpose: tokens CoinGecko does not list (for example stEVMOS, stUMEE, stCMDX, stSOMM, stLUNA and ELYS), vouchers behind a Hub light client that has expired, so they cannot be sent back to their source (CMDX, and USDC routed through Elys, which is not valued at one dollar), and days CoinGecko itself has no price for (some stSTARS and stJUNO days). A price is never carried forward from an earlier day.

Decimals come from the home chain's registry too. On 2026-11-09 this corrected 107 Hub IBC denoms whose decimals had been guessed; stEVMOS and stINJ use 18, not 6. Reports read decimals and prices when they are built, so a report built after that date uses the corrected values.

Gravity DEX

Gravity DEX (the Hub's liquidity module, from July 2021 until Gaia v11 removed it) settled deposits, withdrawals and swaps in a batch at the end of the block, not in the transaction. Those transactions only move coins into the batch escrow, so the settled amounts are read from the block's deposit_to_pool, withdraw_from_pool and swap_transacted events and recorded as swaps. Pool coins are treated as LP tokens: a deposit swaps the reserve coins for pool coins, and a withdrawal swaps pool coins back for reserve coins. Coins the batch returned unused are the wallet's own and are not a swap. Creating a pool settles in the transaction; the pool creation fee paid to the community pool is an expense.

Cosmos Hub before December 2021

From cosmoshub-4 genesis until the Gaia v6 upgrade at block 8,695,000 on 2021-12-14, the Hub ran Cosmos SDK 0.42, which emits no coin_received or coin_spent events. For those blocks amounts are read from the messages and from module events instead: withdraw_rewards for staking rewards, and the distribution module's transfer to the withdraw address for commission. The check that compares the bank module's own totals with the classified rows for movements at the start or end of a block has nothing to compare there, so it does not run. Past-date balances are rebuilt by undoing coin_received and coin_spent events, so a date before the end of 2021-12-14 is not rebuilt.

What holds a report back

A report waits for review when a successful transaction in it has a message type with no reviewed classification. A failed transaction moves only its fee, which is always recorded, so its message types never hold a report back.

Staking rewards versus principal

Delegator reward income includes only executed withdraw_rewards events, split by validator and denomination. Delegated, undelegated and redelegated principal is not income and is recorded separately as delegate, undelegate, redelegate and cancel_unbonding records. Auto-withdrawals during staking operations, including both redelegation validators and liquid staking tokenize and redeem operations, are included. On the Cosmos Hub before about block 14,000,000 (SDK 0.42 to 0.45) a delegate, undelegate or redelegate emitted no withdraw_rewards event for its auto-withdrawal, so the reward is read from the distribution module's transfer to the delegator in that message; when a redelegation withdrew from only one of its two validators, the transfer does not say which, and the reward carries no validator. In an authz exec for several delegators, each inner message's rewards and principal are read from its own events: from authz_msg_index where the chain tagged them, otherwise from the order of the merged log, checked against each inner message's signer and validator, and an exec that cannot be separated that way is held for review rather than guessed. Fees are separate.

Validator attribution

The operator address comes from the withdrawal event. Monikers are current chain REST labels at report generation, not historical names. A validator's jurisdiction is only what its operator account attested on chain (bryanlabs-tax:jurisdiction memo, transaction hash as source); otherwise it is undisclosed. CoinTracker's fixed import schema has no metadata field; use the Generic or Income CSV for validator attribution.

Income recognition policy

at-claimEffective 2026-07-04

Staking rewards and validator commission are income when the recipient has dominion and control over them (Rev. Rul. 2023-14). On Cosmos, earned rewards accrue in the distribution module and are not constructively received until withdrawn: a manual claim, an auto-withdraw triggered by delegate/undelegate/redelegate, or a REStake auto-compound claim. Before that withdrawal the delegator cannot transfer or otherwise exercise control over the reward. We recognize income at that withdrawal event ("at-claim"), valued at its USD price that day. This is the defensible reading of current law for Cosmos, and the architecture is ready for an "at-sale" policy (deferring recognition to disposal) without reindexing, since every reward's own claim date and amount is already stored; only the query-time recognition rule would change. Pending legislation (e.g. the Lummis bill, House Ways & Means drafts) could move staking rewards to taxed-at-sale; if enacted, that will be added as a selectable policy.

Where validator commission goes

Validator statements include a trace of each commission withdrawal: from the wallet it was paid to, through transfers, delegations, unbonding and IBC sends, up to three wallets further. Coins mixed with other coins are split by two rules, first in first out and pro-rata, and both are published because neither is the truth. Exchange destinations come only from labeled addresses with a named source; addresses inferred from deposit-style memos are reported separately. USD is the value when the commission was withdrawn. The trace describes on-chain movements, not intent, and is never used to rank validators. Hub-wide view.

ATOM/USD price series

The daily reference price series is generated by this same methodology, not a separate calculation, so it can never drift from what a report actually charged a given day.