Wallet API for people and agents

Holdings, gain/loss and transaction history for any wallet

Send any EVM, Solana or Bitcoin address and get JSON back: its coins and NFTs priced in USD, what each coin cost and how far it's up or down, and its confirmed transactions with USD values at the time.

From $0.01 a call. No subscription. Pay per call in USDC over x402 with no account, or by card with an API key.

GET https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 200
{
  "resolvedAddress": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
  "tokens": [
    {
      "type": "fungible",
      "symbol": "USDC",
      "network": { "name": "Base", "id": "base", … },
      "balance": "6120.5",
      "currentPrice": "0.999819",
      "value": "6119.3921895",
      "change": { "oneHour": "0.000687", "oneDay": "-0.000094",
                  "oneWeek": "0.003697" },
      …
    },
    …
  ],
  "timestamp": 1791676800000,
  "pricesUpdatedOnly": true
}
$0.05 a call by default. Coins first, by USD value, then NFTs. Illustrative values, shortened.
chains
12 EVM chains, Solana and Bitcoin
input
Any 0x, Solana or Bitcoin address; ENS names resolve automatically
pay
USDC on Base, Ethereum or Solana via x402, or a card
price
$0.01 to $0.20 a call, with no base fee

Products

Three products

Holdings and transaction history are separate requests. Gain/loss is added to a holdings call, from $0.11 in total. All three take the same addresses and the same API key or x402 payment. Agents can read the full reference as markdown at /llms.txt.

Holdings

GET /{address}

A wallet's coins and NFTs with USD prices, in one JSON document.

  • Coins with balance, USD price, USD value and 1h, 24h and 7d change
  • NFTs with images, sorted by collection
  • One 0x address checked on all 12 EVM chains at once; Solana and Bitcoin addresses too
  • Pick freshness per call: the cached result, new prices on the last balances, or a full re-read of every chain

$0.05a call, by default

$0.01 for a cached result, $0.10 to read every chain again.

curl https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 -H "X-Api-Key: $INVY_API_KEY"
Holdings reference

Gain/loss

?gainLoss=true

What each coin cost and how far it's up or down, added to any holdings call.

  • Cost basis, average cost and unrealized gain or loss for each coin
  • Wallet totals over every coin with a result
  • Worked out first in, first out from each coin's transfer history
  • The wallet's 50 most valuable coins worth $1 or more

+$0.10on any holdings call

$0.15 with the default holdings call. Estimates, not tax records.

curl "https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045?gainLoss=true" -H "X-Api-Key: $INVY_API_KEY"
Gain/loss reference

Transaction history

GET /{address}/transactions

A wallet's confirmed transactions, newest first, with USD values at the time.

  • Every transfer in and out, with its price and value when it happened
  • The network fee, when the wallet paid it
  • A type for each (send, receive, swap, nft and more) and address-poisoning flags
  • Filter EVM networks or set a start time; page with a cursor

$0.05a page of up to 25

Empty follow-up pages aren't charged.

curl https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/transactions -H "X-Api-Key: $INVY_API_KEY"
Transaction history reference

Pricing

No subscription. Pay only for what you use.

Pay per call in USDC with x402, with no account. Or put calls on a card with an API key, billed monthly for what you used. Prices are the same either way.

Running your own nodes, indexers and price feeds costs about $1,000 a month. Here, a call starts at $0.01.

Price of each request
RequestPrice
Holdings GET /{address}
action=cached-okay

The last result built for this wallet, however old; check timestamp. With no cached result, a full fetch.

$0.01a call 1 credit
action=refresh-prices default

Balances from the wallet's last fetch, with current prices. The first call for a wallet fetches balances.

$0.05a call 5 credits
action=refresh-holdings

Balances and NFTs read again from every chain, with current prices.

$0.10a call 10 credits
Gain/loss gainLoss=true
Added to any holdings call

Cost basis and unrealized gain or loss for each coin, plus wallet totals: $0.11, $0.15 or $0.20 a call in total. Charged on every call, including calls where some coins are still pending.

+$0.10a call 10 credits
Transaction history GET /{address}/transactions
One page of up to 25 transactions

Newest first, with USD values at the time of each. Empty follow-up pages aren't charged; see which pages are charged.

$0.05a page 5 credits
  • Prices are in credits, whichever way you pay: 1 credit = $0.01. 1,000 default holdings calls cost $50, or $150 with gain/loss. 1,000 pages of transactions cost $50.
  • Not charged: names that don't resolve, invalid input, error responses (4xx and 5xx statuses), empty transaction pages after the first, and empty first pages where a network couldn't be fully read.
  • A holdings call that hits the ~50-second limit returns what has loaded, marked incomplete: true, at full price.
  • Charged responses carry an X-Invy-Credits header with the credits the call cost.

Two ways to pay

No account

x402, per call

Call the endpoint with no credentials and it answers 402 Payment Required with a PAYMENT-REQUIRED header listing the payments it accepts. Your x402 client signs a USDC payment for that one call and retries with a PAYMENT-SIGNATURE header.

Pay with
USDC on Base, Ethereum or Solana
Settled
After the response is delivered. Failed lookups are never settled.
Good for
Agents that hold their own wallet, and occasional calls
How x402 works

If a request carries both an API key and an x402 payment, the key pays and the x402 payment is never settled. An invalid key gets 401 even when a payment is attached.

https://invy.fi

API reference

Three read-only GET requests on one host. They take the same addresses and the same authentication. Also available as plain markdown at /llms.txt.

Requests and addresses

One request per product
# Holdings
GET https://invy.fi/{address}

# Gain/loss, on any holdings call
GET https://invy.fi/{address}?gainLoss=true

# Transaction history
GET https://invy.fi/{address}/transactions

The old host, https://invy.bot/{address}, still answers API calls, so existing integrations keep working.

{address} can be any of these, on every product:

EVM address
0x followed by 40 hex characters. Checked on all 12 EVM chains.
Solana address
A base58 address. SOL, SPL tokens and Solana NFTs.
Bitcoin address
Holdings return the BTC balance only; transaction history covers BTC transfers only.
ENS name
A name like vitalik.eth, or a Basename like name.base.eth. Resolved to its EVM address automatically.

Authentication

X-Api-Keyheader
Your API key, invy_sk_…. Billed to your card. Create keys in the dashboard. Keep keys on your server; don't ship them to browsers or apps.
PAYMENT-SIGNATUREheader
A base64 x402 payment for this call. X-PAYMENT is also accepted.

Send neither and you get 402 with a PAYMENT-REQUIRED header. See Paying with x402.

Time limits and headers

Every call answers within about 50 seconds, so give your client a timeout of 60 seconds or more. Successful responses are 200 with Content-Type: application/json. Charged responses carry an X-Invy-Credits header with the credits the call cost, whichever way you pay. Responses can include fields not listed here; ignore the ones you don't use.

GET /{address}

Holdings

A wallet's coins and NFTs, with USD prices. $0.01, $0.05 (default) or $0.10 a call, set by action.

Request

Holdings URL
GET https://invy.fi/{address}

Query parameters

actionstring, optional
cached-okay, refresh-prices (default) or refresh-holdings. Sets what the call returns and what it costs; see Pricing. Any other non-empty value is treated as cached-okay.
gainLossstring, optional
true adds cost basis and unrealized gain or loss for each coin, plus wallet totals, for $0.10 more. Only the exact value true turns it on. See Gain/loss.

Response

A full fetch of a large wallet can take up to about 50 seconds. Chains still loading at that point are left out and the response has incomplete: true.

inputAddressstring
The {address} you sent. On a cached-okay response, the rest of the result is as it was when first built and may be old; check timestamp.
resolvedAddressstring
The address that was looked up. EVM addresses are lowercase.
tokensarray
Coins sorted by USD value, highest first, then NFTs sorted by collection and name.
timestampnumber
When this result was built, in milliseconds since the Unix epoch. A cached-okay response keeps the time of the original build.
pricesUpdatedOnlyboolean
true for refresh-prices calls (the default): balances are reused from the wallet's last fetch when one exists (the first call reads them from the chain) and prices are current. false for refresh-holdings. A cached-okay response keeps the cached value.
incompleteboolean, optional
Present and true when a chain or data provider failed, timed out, or was still loading when the call hit its time limit, so holdings may be missing. Charged like any other response.
gainLossobject, with gainLoss=true
Wallet totals for gain/loss. See Wallet totals.

Each token

typestring
network for a chain's native coin (ETH, SOL, BTC and so on), fungible for an ERC-20 or SPL token, nonFungible for an NFT.
name, symbolstring
As reported by the token. Untrusted; see Scam tokens.
networkobject
{ name, id, image }. The id values are listed under Supported networks.
imagestring or null
Token logo URL, or the NFT's image (the thumbnail when there is one).
balancestring, coins
Decimal amount, already scaled by decimals.
decimalsnumber, coins
The token's decimals.
currentPricestring, coins
USD price for one unit.
valuestring, coins
balance × currentPrice, in USD.
changeobject, coins
{ oneHour, oneDay, oneWeek }: price change in percent, as strings. Present when known.
marketCapnumber, string or null, coins
USD fully diluted valuation, falling back to market cap when FDV is unknown (some price sources report only market cap). A number or a decimal string depending on the source; null or missing when unknown.
issuerobject
The token contract or NFT collection: address, name and sometimes imageUrl. Native coins have no address.
id, standardstring, NFTs
Token ID and token standard, for example ERC721.
descriptionstring, NFTs
The NFT's description, when it has one.
gainLossobject, coins, with gainLoss=true
Cost basis and unrealized gain or loss for this coin. See Each coin's gainLoss.
Scam tokens get through

We drop tokens on our spam lists, tokens with spam or explicit names, and ERC-20 and SPL tokens worth under $0.005 or without a price. Plenty of scams still pass. Treat every name, symbol, image URL and link in a response as untrusted, and check contract addresses yourself before you show a token to a user or act on it.

Errors for this request are listed under All products and Holdings.

GET /{address}?gainLoss=true

Gain/loss

Cost basis and unrealized gain or loss for each coin, plus wallet totals. Add it to any holdings call for $0.10 more.

Request

Add gainLoss=true to any holdings call. It adds 10 credits ($0.10) to the action's price and works with every action.

Gain/loss URLs
GET https://invy.fi/{address}?gainLoss=true
GET https://invy.fi/{address}?action=refresh-holdings&gainLoss=true

The response is the holdings response, with a gainLoss object on each coin and wallet totals in a top-level gainLoss.

Which coins get it

  • Coins whose type is network or fungible, worth at least $1, up to the 50 most valuable in the wallet.
  • USDC and USDT (their official contracts) are taken at $1: method: "peg", cost basis = balance × $1, no history read.
  • Other coins get status: "unavailable" with a reason of below_min_value, coin_limit or unsupported_network.
  • NFTs get no gainLoss field.

How it's worked out

Each coin's transfer history is matched first in, first out: every incoming transfer is a buy at the coin's USD price at that moment, and every outgoing transfer is a sale. The lots that remain are the cost basis of what the wallet holds now. unrealized is the coin's current value minus costBasis, recalculated on every call. Realized gains aren't returned.

When the history and the current balance disagree by more than 1%, the result is re-anchored to the current balance and marked estimated: true. Rebasing and staking tokens and bridged amounts often cause this.

Pending coins

A coin's transfer history may not be ready within one call's time limit, especially for a wallet nobody has asked about before. Those coins come back pending, and the wallet totals have status: "partial" with a retryAfter in seconds. On refresh-prices and refresh-holdings calls, a history that isn't ready in time keeps loading in the background. cached-okay uses whatever history is already cached, however old, and doesn't wait: for a coin with no cached history, it starts loading one in the background. Either way, pending coins fill in on later calls, and each of those calls is charged.

Every call with gainLoss=true is charged the full price, including calls where some or all coins are pending or unavailable. There is no free retry: each later call that fills in pending coins is charged too.

Each coin's gainLoss

Money values are decimal strings in USD, like value.

statusstring
ok, pending or unavailable.
reasonstring, when unavailable
Why there's no result. See Reasons.
costBasisstring or null
What the coins the wallet still holds cost, in USD. null unless status is ok, like the next three fields.
avgCoststring or null
Cost per coin: costBasis divided by the balance.
unrealizedstring or null
value minus costBasis. Negative is a loss.
unrealizedPercentstring or null
unrealized as a percent of costBasis, as a string number such as "-5.07". Also null when costBasis is "0.00", as for an airdrop or a coin bought for under a cent.
methodstring or null
How costBasis was worked out. See Methods. null unless status is ok.
estimatedboolean
true when the result is an estimate rather than an exact lot-by-lot match: every method except fifo, including peg.
historyAsOfnumber or null
When the transfer history behind the result was built, in milliseconds since the Unix epoch. On a pending coin, the time of an older cached history if there is one.
transfersnumber or null
How many transfers that history holds.

Reasons

no_buy_history
The history has no incoming transfers to take a cost from.
missing_buy_price
A buy has no USD price for its time. Common for illiquid and new tokens, especially on Solana.
history_incomplete
The history hit its size limit (see Caveats), or a data provider failed.
sales_exceed_buys
More of the coin went out than came in, so lots can't be matched.
unsupported_network
We have no transfer history source for this coin's network.
below_min_value
The coin is worth less than $1.
coin_limit
The coin isn't among the wallet's 50 most valuable coins.

Methods

fifo
Lots matched first in, first out, and the history agrees with the balance.
remaining_lots_avg
The history and the balance disagree by more than 1%: the average cost of the lots still held, times the current balance.
all_time_avg
Every lot was matched against a sale but a balance remains: the average price of all buys, times the current balance.
peg
USDC and USDT: the balance at $1.

Wallet totals: top-level gainLoss

statusstring
complete, or partial while any coin is pending.
costBasis, unrealized, unrealizedPercentstring or null
Totals over the coins whose status is ok, in the same formats as each coin's fields. null when no coin is ok. unrealizedPercent is also null when the total costBasis is "0.00".
valueCoveredstring or null
The current USD value of those ok coins, the holdings the totals describe. null when no coin is ok.
coinsobject
Coin counts: eligible, ok, estimated, pending and unavailable.
retryAfternumber, when partial
Seconds to wait before calling again for the pending coins.
Response with gainLoss=true, shortened
{
  "resolvedAddress": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
  "tokens": [
    {
      "type": "network",
      "symbol": "ETH",
      "balance": "1.25",
      "currentPrice": "4000",
      "value": "5000",
      "gainLoss": {
        "status": "ok",
        "costBasis": "3750.00",
        "avgCost": "3000.00",
        "unrealized": "1250.00",
        "unrealizedPercent": "33.33",
        "method": "fifo",
        "estimated": false,
        "historyAsOf": 1791676500000,
        "transfers": 37
      },
      …
    },
    {
      "symbol": "USDC",
      "value": "250.07",
      "gainLoss": { "status": "ok", "costBasis": "250.12", "unrealized": "-0.05", "method": "peg", … },
      …
    },
    {
      "symbol": "AERO",
      "gainLoss": { "status": "pending", "costBasis": null, … },
      …
    }
  ],
  …
  "gainLoss": {
    "status": "partial",
    "costBasis": "4000.12",
    "unrealized": "1249.95",
    "unrealizedPercent": "31.25",
    "valueCovered": "5250.07",
    "coins": { "eligible": 3, "ok": 2, "estimated": 1, "pending": 1, "unavailable": 0 },
    "retryAfter": 60
  }
}
Illustrative values. The wallet totals cover ETH and USDC; AERO is still loading.
Estimates, not tax records
  • Every incoming transfer counts as a buy and every outgoing transfer as a sale, at the market price at that moment. That includes transfers between your own wallets, airdrops and bridged funds.
  • Gas and other network fees aren't added to cost basis.
  • Each coin's history stops at about 2,000 transfers (about 1,000 on some networks). Coins past that come back history_incomplete, so the most active coins are hit hardest.
  • Only unrealized gain or loss is returned.
  • pending coins fill in on later calls, and each of those calls is charged.

GET /{address}/transactions

Transaction history

A wallet's confirmed transactions, newest first, up to 25 to a page, with USD values at the time of each. $0.05 a page.

Request

Transaction history URL
GET https://invy.fi/{address}/transactions

{address} takes the same addresses and names as holdings, and the same authentication: an X-Api-Key header, or an x402 payment of 5 credits ($0.05) per page.

Query parameters

limitinteger, optional
Transactions per page, from 1 to 25. Default 25.
cursorstring, optional
The nextCursor from the previous page, txc_…. Leave it out for the first page.
networksstring, optional
Comma-separated network ids, for example ethereum,base. EVM addresses only; the default is every EVM network. Ids are listed under Supported networks.
sincestring or number, optional
An ISO-8601 time such as 2026-09-01T00:00:00Z, or milliseconds since the Unix epoch. Only transactions from that time on are returned, and paging ends there.

Parameters are checked before payment, so a malformed request gets a 400 straight away and is never charged.

Response

inputAddressstring
The {address} you sent.
resolvedAddressstring
The address that was looked up. EVM addresses are lowercase.
transactionsarray
Newest first. See Each transaction.
nextCursorstring or null
Pass it as cursor to get the next page. null exactly when hasMore is false.
hasMoreboolean
Whether older transactions remain.
networksobject
One entry per network id: { status, reason? }. See Network status.
incompleteboolean
true when any network's status is error or partial, so the page may be missing transactions or legs.
timestampnumber
When this page was built, in milliseconds since the Unix epoch.

Network status

ok
Read for this page. Older history may remain.
done
No older history on this network, or none since since.
filtered
Left out by networks.
partial
The network was read, but some of its data is missing, or it couldn't be read for this page. reason says why when known: provider_error (a data provider failed) or deadline (it ran out of time).
error
The network failed on 3 pages in a row, so later pages stop reading it and its older transactions aren't returned, even when hasMore is false. reason is provider_error. To try it again, start over without a cursor.

A network in partial keeps its place, and later pages keep reading it. After 3 failed pages in a row it becomes error and is left out of the rest of the paging.

Each transaction

idstring
Identifies the transaction; treat it as opaque. The same id can end one page and start the next. Merge their transfers.
hashstring
The transaction hash, or signature on Solana.
networkobject
{ id, name }.
timestampnumber or null
When it was confirmed, in milliseconds since the Unix epoch.
blockNumbernumber or null
The block it was confirmed in, when known.
typestring
send, receive, swap, nft, contract_call, self (the wallet sent to itself) or other. A best guess from the transfers; check transfers when it matters.
feeobject or null
{ amount, symbol, valueAtTime }: the network fee, present only when this wallet paid it.
transfersarray
Each movement of value, below.
flagsarray of strings
Warnings, for example address_poisoning: the transaction looks like an attempt to plant a look-alike address in the wallet's history. Usually empty.

Each transfer

directionstring
in or out, relative to the wallet.
from, tostring or null
Addresses.
fromName, toNamestring or null
The address's primary name, such as an ENS name, when it has one.
assetobject
type, symbol, name, address, tokenId and decimals. type is network, fungible or nonFungible, as in holdings. address is the token contract, null for a native coin; tokenId is set for NFTs. decimals is null for NFTs. Names and symbols are untrusted, as in holdings.
amountstring or null
Decimal amount, already scaled by decimals.
priceAtTimestring or null
USD price for one unit when the transaction happened. null when unknown, and for NFTs.
valueAtTimestring or null
amount × priceAtTime, in USD.
Response, shortened
{
  "inputAddress": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  "resolvedAddress": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
  "transactions": [
    {
      "id": "base:0x6f1c…e2a9",
      "hash": "0x6f1c…e2a9",
      "network": { "id": "base", "name": "Base" },
      "timestamp": 1791589843000,
      "blockNumber": 36410022,
      "type": "swap",
      "fee": { "amount": "0.000004127", "symbol": "ETH", "valueAtTime": "0.0168757982" },
      "transfers": [
        {
          "direction": "out",
          "from": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
          "fromName": "vitalik.eth",
          "to": "0x6ff5…9b43",
          "toName": null,
          "asset": { "type": "fungible", "symbol": "USDC", "name": "USD Coin", "address": "0x8335…2913", "tokenId": null, "decimals": 6 },
          "amount": "250",
          "priceAtTime": "1",
          "valueAtTime": "250"
        },
        { "direction": "in", "asset": { "type": "network", "symbol": "ETH", … }, "amount": "0.0611", "priceAtTime": "4089.12", "valueAtTime": "249.845232", … }
      ],
      "flags": []
    },
    …
  ],
  "nextCursor": "txc_Q2lQd3V6VnJ4S0h1aG5zZw",
  "hasMore": true,
  "networks": { "ethereum": { "status": "ok" }, "base": { "status": "ok" }, "arbitrum": { "status": "filtered" }, … },
  "incomplete": false,
  "timestamp": 1791676800123
}
Illustrative values for ?networks=ethereum,base.

Pagination

  • Pages run newest to oldest. Pass nextCursor as cursor on the same address until hasMore is false.
  • Pages aren't a fixed snapshot. A later page can include a transaction that confirmed after the first page, on a network the earlier pages had no transactions from yet, so it can be newer than ones already returned. Sort by timestamp and de-duplicate by id if you need strict order. To see new activity, start again without a cursor, or with since.
  • A cursor remembers the first page's address, networks and since. Send the same values, or leave networks and since off; anything different returns 400 cursor_mismatch.
  • Cursors expire after 7 days; an expired or unknown cursor returns 400 invalid_cursor.
  • A page can hold fewer than limit transactions even when hasMore is true. Keep paging.
  • Repeating a request returns the same page from our cache: for 60 seconds on a first page, and up to 24 hours on a cursor page. Pages with incomplete: true and very large pages aren't cached, so a repeat builds them again. Repeats are charged like any page.

Charging

PageCharged
At least one transaction, including pages with incomplete: true5 credits
First page, no transactions, every network ok, done or filtered: the wallet has no history5 credits
Cursor page with no transactionsNo
First page, no transactions, some network in error or partialNo
Any 4xx or 5xx responseNo

Coverage

What a page includes
  • Confirmed only. Pending transactions never appear, and the newest few blocks can lag.
  • Value transfers. Transactions are built from movements of coins and NFTs. Approvals and other calls that move nothing may be missing, or appear as contract_call.
  • USD at the time. Native coins use an hourly price, so they can be off by up to an hour. USDC and USDT are taken at $1. NFTs have no price. Unknown prices are null.
  • Spam dropped. Transactions involving spam tokens are left out, as are some small unpriced incoming transfers on Solana.
  • Partial networks. On some layer-2 networks, transfers of the native coin made inside contracts come from a separate source; when it fails, the network shows partial. ZkSync, Scroll and Arbitrum Nova never include those transfers, and Bitcoin covers BTC transfers only.
  • No perps. Perpetual futures activity isn't included.

Errors for this request are listed under All products and Transaction history.

4xx · 5xx

Errors

Shared by all three products, plus a few of each product's own. Error responses are never charged.

Keyed errors, the x402 errors payment_already_used and invalid_payment, internal_error, and transaction history's own errors are JSON objects with an error code you can branch on and a message. Other x402 errors, on every product, are a JSON string.

All products

401invalid_api_key
The key is unknown or revoked. Returned even when an x402 payment is attached.
402billing_required
The key is valid but card billing isn't active on the account. Add a card in the dashboard.
402"Payment Required"
No API key and no payment header. The PAYMENT-REQUIRED header lists accepted payments.
402"<reason>"
A payment header was sent but couldn't be decoded or failed verification. The body is the reason, for example "Invalid payment header", "Unsupported network: …" or the facilitator's reason. There is no PAYMENT-REQUIRED header and nothing is charged. Don't resend the same payment; send the request without one to get the requirements again.
402payment_already_used
This x402 payment was already used for another request, including one sent at the same time. Each signed payment pays for one request, even if that request failed and wasn't settled. Not charged; sign a new payment.
402invalid_payment
The payment payload has no authorization nonce or signed transaction. Not charged; sign a new payment.
429spend_cap_reached
This billing period's spend reached the monthly limit set in your dashboard. Calls resume when the next period starts, or as soon as you raise the limit. The body also has capUsd, spentUsd and resetsAt (Unix seconds), with a Retry-After header.
500internal_error
A temporary server error. Not charged; retry, and with x402 sign a new payment for the retry.

Holdings

A holdings lookup that can't start answers 200 with an error field and no wallet data.

200{"error": "Could not resolve address"}
The name didn't resolve to an address. Not charged.
200{"error": "Invalid input"}
The path contains characters that aren't allowed. Not charged.

Transaction history

These are JSON objects, { error, message }, with a real status code, whichever way you pay. Unlike holdings, a name that doesn't resolve is a 400, not a 200. Malformed parameters are rejected before payment; the other errors come after an x402 payment is checked, so sign a new payment to retry.

400invalid_address
Not a valid EVM, Solana or Bitcoin address, or a name that doesn't resolve.
400invalid_limit
limit isn't a whole number from 1 to 25.
400invalid_networks
An unknown network id, or networks sent for an address that isn't EVM.
400invalid_since
since isn't an ISO-8601 time or a millisecond timestamp.
400invalid_cursor
The cursor is malformed, unknown or expired. Start again from the first page.
400cursor_mismatch
The cursor was made for a different address or different filters.
503upstream_timeout
A data provider, or resolving a name, took too long. Not charged; retry.
503upstream_unavailable
A data provider failed or transaction history is briefly unavailable. Not charged; retry shortly.

curl · fetch · x402

Examples

Swap 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 for any supported address. ENS names like vitalik.eth work too and resolve automatically.

Holdings

curl · $0.10
curl "https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045?action=refresh-holdings" \
  -H "X-Api-Key: $INVY_API_KEY"
JavaScript · Node 18+, ES module · $0.05
const res = await fetch('https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', {
  headers: { 'X-Api-Key': process.env.INVY_API_KEY },
});
if (!res.ok) throw new Error(`invy.fi answered ${res.status}`);

const wallet = await res.json();
if (wallet.error) throw new Error(wallet.error);

const coins = wallet.tokens.filter((token) => token.type !== 'nonFungible');
const usd = coins.reduce((sum, token) => sum + Number(token.value ?? 0), 0);
console.log(`${wallet.resolvedAddress} holds $${usd.toFixed(2)} in coins`);

Gain/loss

curl · $0.15
curl "https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045?gainLoss=true" \
  -H "X-Api-Key: $INVY_API_KEY"
JavaScript · Node 18+, ES module · $0.15
const res = await fetch('https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045?gainLoss=true', {
  headers: { 'X-Api-Key': process.env.INVY_API_KEY },
});
if (!res.ok) throw new Error(`invy.fi answered ${res.status}`);

const wallet = await res.json();
if (wallet.error) throw new Error(wallet.error);

for (const token of wallet.tokens) {
  const gl = token.gainLoss;
  if (gl?.status !== 'ok') continue;
  const note = gl.estimated ? ' (estimated)' : '';
  const pct = gl.unrealizedPercent === null ? '' : ` (${gl.unrealizedPercent}%)`;
  console.log(`${token.symbol}: cost $${gl.costBasis}, unrealized $${gl.unrealized}${pct}${note}`);
}

const { status, unrealized, retryAfter } = wallet.gainLoss;
if (unrealized !== null) console.log(`Wallet unrealized: $${unrealized}`);
if (status === 'partial') console.log(`Some coins are pending; a call in ${retryAfter} s may fill them in (and is charged)`);

Transaction history

curl · $0.05
curl "https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/transactions?limit=25&networks=ethereum,base" \
  -H "X-Api-Key: $INVY_API_KEY"
JavaScript · Node 18+, ES module · up to $0.20
const url = new URL('https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/transactions');
url.searchParams.set('networks', 'ethereum,base');
url.searchParams.set('since', '2026-09-01T00:00:00Z');

const byId = new Map();
for (let page = 1; page <= 4; page++) {
  const res = await fetch(url, { headers: { 'X-Api-Key': process.env.INVY_API_KEY } });
  const body = await res.json();
  if (!res.ok) throw new Error(`invy.fi answered ${res.status}: ${body.error}`);

  for (const tx of body.transactions) {
    const seen = byId.get(tx.id);
    if (seen) seen.transfers.push(...tx.transfers);
    else byId.set(tx.id, tx);
  }
  if (!body.hasMore) break;
  url.searchParams.set('cursor', body.nextCursor);
}
console.log(`${byId.size} transactions since September 1`);

The loop sends the same networks and since with every cursor, merges an id that spans two pages, and stops after 4 pages, so it costs at most 20 credits ($0.20).

Paying with x402

Any x402 v2 client handles this for you; the spec and client libraries are at docs.x402.org. Without a key or payment, the first round trip looks like this:

First request
$ curl -i https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
HTTP/2 402
content-type: application/json
payment-required: eyJ4NDAyVmVyc2lvbiI6…

"Payment Required"
PAYMENT-REQUIRED, base64-decoded and shortened
{
  "x402Version": 2,
  "resource": {
    "url": "https://invy.fi/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    "mimeType": "application/json",
    …
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "amount": "50000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0x19c5fbc520a3a8c2fa440148193de4543a5fc66a",
      "maxTimeoutSeconds": 120,
      …
    },
    { "scheme": "exact", "network": "eip155:1", … },
    { "scheme": "exact", "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", … }
  ]
}

amount is in USDC base units (6 decimals), so 50000 is $0.05, the default holdings price. Sign one of the accepts entries and send the same request again with a PAYMENT-SIGNATURE header. The payment is settled after the response is delivered.

  • amount is the call's credits × 10,000: 10000 for cached-okay, 50000 for refresh-prices, 100000 for refresh-holdings, 100,000 more with gainLoss=true, and 50000 for a page of transactions.
  • The price comes from the URL, so retry the exact URL that returned the 402. A payment signed for a different price fails verification with a 402 and isn't charged.
  • Each signed payment pays for one request. Sending it again returns 402 payment_already_used, even if the first request failed and was never settled, so sign a new payment to retry.

network.id

Supported networks

Names and ids as they appear in token.network, in each transaction's network and in the networks parameter. One EVM address is checked on every EVM chain in a single call.

  • Ethereumethereumcoins, NFTs
  • Basebasecoins, NFTs
  • Arbitrumarbitrumcoins, NFTs
  • Optimismoptimismcoins, NFTs
  • Polygonpolygoncoins, NFTs
  • Avalancheavalanchecoins, NFTs
  • ZkSynczksynccoins, NFTs
  • Linealineacoins, NFTs
  • Scrollscrollcoins, NFTs
  • Binance Smart Chainbsccoins
  • Gnosisgnosiscoins
  • Arbitrum Novaarbitrum_novacoins
  • Solanasolanacoins, NFTs
  • BitcoinbitcoinBTC balance

FAQ

Before you integrate

Do I need an account?

No. Without a key, each call is paid over x402 in USDC. An account is only for paying by card with API keys.

Which calls are billed?

Every holdings response that carries wallet data, at its action's price, including responses with incomplete: true, such as a large wallet whose chains were still loading at the time limit (about 50 seconds). Names that don't resolve, invalid input and error statuses (4xx and 5xx) aren't charged.

With gainLoss=true, every such response costs $0.10 more, even when some coins are still pending. Transaction history costs $0.05 a page; empty follow-up pages and some empty first pages are free (see Charging).

How do I stop paying?

Stop calling and you pay nothing: there's no base fee and no minimum. To end card billing, open Manage billing in the dashboard any time. Usage up to the end of that billing period is billed as usual, and after that keyed calls return 402 billing_required.

Can I use gain/loss for taxes?

No. It treats every incoming transfer as a buy and every outgoing transfer as a sale at the market price, including moves between your own wallets, leaves out gas, and returns unrealized gain or loss only. Use it to show positions, not to file returns.

How fresh is the data?

refresh-holdings reads balances from each chain again. refresh-prices reuses the balances from the wallet's last fetch and updates prices, so call refresh-holdings after the wallet transacts. cached-okay returns the last result as it was; its timestamp tells you how old it is.

Transaction history shows confirmed transactions; the newest few blocks can lag, and a repeated first-page request returns the same page for 60 seconds.

How do I delete my account?

Email hello@invy.fi from the address you sign in with.