# Crypto portfolio tracker API: build on read-only keys

URL: https://fincodelabs.com/en/blog/crypto-portfolio-tracker-api · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Fintech development · Published: 2026-11-05 · Updated: 2026-11-05

Crypto portfolio tracker API guide: read-only keys, balances, fills and transfers endpoints, pagination, rate limits, an append-only ledger and average cost.

**Short answer:** A **crypto portfolio tracker API** integration reads balances, fills and transfers from each exchange with read-only keys, stores them in an append-only ledger, computes positions and average cost, and reconciles the result against the balances each exchange reports. It needs no trading or withdrawal permission and places no orders. This is reporting software for your own holdings, which is a different thing from managing a portfolio for someone else.

Reading account data is the lowest-risk way to use an exchange API, because a read-only key cannot place an order or move funds. The difficulty lies elsewhere: histories arrive in pages with awkward time windows, fees come in other assets, transfers look like losses until they are paired, and a total means little until it has been reconciled.

This guide covers the build for product and engineering leads. Endpoints come from official documentation read on 4 November 2026, in alphabetical order, as examples and not recommendations. Tracker apps are neither named nor ranked. Key handling in general is covered in [API key security](https://fincodelabs.com/en/blog/api-key-security-trading-bots) and request signing in [crypto exchange API integration](https://fincodelabs.com/en/blog/crypto-exchange-api-integration).

## How does a crypto portfolio tracker work?

A tracker is a one-way pipeline from exchange APIs to reports, and each stage can be tested on its own.

1. **Connectors** call each exchange with a read-only key and keep a checkpoint per account, endpoint and symbol.
2. **Raw store** keeps every response unchanged, so a parsing bug can be fixed by replaying history.
3. **Normalizer** maps venue formats to one event type.
4. **Ledger** holds those events as immutable rows.
5. **Calculator** derives quantity, average cost and realized result.
6. **Reconciliation and reports** compare the result with reported balances and publish totals with an as-of time.

A spreadsheet or a project from GitHub can be enough for a few accounts. Before adopting one, read its license and check where it stores keys. Custom work starts to make sense when several venues, sub-accounts or auditors are involved.

## Read-only API keys: which permissions to grant

Grant only read permissions, and make the software verify this itself at start-up where the exchange exposes it.

**Read access in official documentation, 4 November 2026 (alphabetical)**

| Exchange | Permission to grant | Can software check it? |
| --- | --- | --- |
| Binance | Enable Reading only | Yes: `GET /sapi/v1/account/apiRestrictions` |
| Coinbase | `view` only | Yes: `GET /api/v3/brokerage/key_permissions` |
| Kraken | Query Funds, Query Open Orders & Trades, Query Closed Orders & Trades, Query Ledger Entries | Yes: `POST /0/private/GetApiKeyInfo` returns a `permissions` list |

This guard refuses any key that could do more than read. It was tested on the synthetic responses at the bottom:

```python
def binance_key_is_read_only(r):
    """r is the response of GET /sapi/v1/account/apiRestrictions."""
    writes = ["enableWithdrawals", "enableInternalTransfer", "permitsUniversalTransfer",
              "enableSpotAndMarginTrading", "enableMargin", "enableFutures",
              "enableVanillaOptions", "enableFixApiTrade", "enablePortfolioMarginTrading"]
    return r.get("enableReading") is True and not any(r.get(f) for f in writes)

def coinbase_key_is_read_only(r):
    """r is the response of GET /api/v3/brokerage/key_permissions."""
    return r.get("can_view") is True and not r.get("can_trade") and not r.get("can_transfer")

print(binance_key_is_read_only({"enableReading": True, "enableSpotAndMarginTrading": True}))
print(coinbase_key_is_read_only({"can_view": True, "can_trade": False, "can_transfer": False}))
```

It prints `False` and then `True`. On Kraken, apply the same idea to the documented `permissions` list. Add an IP restriction as well, because a read-only key still exposes balances and trade history. Readers using platforms on the Turkish regulator's list can find their documented APIs in the [Turkish crypto exchange APIs](https://fincodelabs.com/en/blog/turkish-crypto-exchange-apis) guide.

## Balances, fills and transfers: which endpoints to call

A complete record needs three kinds of data, and each exchange splits them differently.

**Endpoints and paging in official documentation, 4 November 2026**

| Data | Binance Spot | Coinbase | Kraken Spot |
| --- | --- | --- | --- |
| Balances | `GET /api/v3/account`, weight 20 | [`GET /api/v3/brokerage/accounts`](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/accounts/list-accounts), up to 250 per page, cursor | `Balance`, `BalanceEx` |
| Fills | `GET /api/v3/myTrades`, per symbol, up to 1,000 per call, `fromId`; weight 20 | `GET /api/v3/brokerage/orders/historical/fills`, cursor | `TradesHistory`, 1 to 100 per page, `with_cursor`; `ofs` is deprecated |
| Transfers | `GET /sapi/v1/capital/deposit/hisrec` and `GET /sapi/v1/capital/withdraw/history`, up to 1,000 per call, windows under 90 days | `GET /v2/accounts/:account_id/transactions`, `view` scope | `Ledgers`, 50 per page, `with_cursor` |

Three details cause most gaps:

- **Windows.** On Binance, `myTrades` with a start and end time cannot span more than 24 hours, so page by `fromId`. Transfer history needs a walk through 90-day windows.
- **Symbols.** Binance trades are requested per symbol, so the connector must know every symbol the account has traded.
- **Fees.** A Binance fill reports `commission` and `commissionAsset`, which may differ from either side of the trade. Store the fee as its own movement.

Also list what the tracker does not cover, such as futures, earn products or other sub-accounts, so a total is never mistaken for the whole account.

## Pagination and rate limits: planning a backfill

The first import is the expensive part, so run it as a resumable job with checkpoints and a request budget.

Limits differ in kind. Binance counts request weight, with 6,000 per minute in live `exchangeInfo` on 4 November 2026, and returns HTTP 429 with `Retry-After` when exceeded. Kraken's documentation describes a call counter per key where ledger and trade history calls add 2, and the counter decays by 0.33 to 1 per second depending on tier; going over returns `EAPI:Rate limit exceeded`.

> **Hypothetical example.** A Kraken account has 12,000 ledger entries. At 50 per page that is 240 calls, adding 480 to the counter. At the Starter decay of 0.33 per second, the import takes about 24 minutes; at the Pro decay of 1 per second, about 8 minutes. On Binance, a sweep of 50 symbols at weight 20 uses 1,000 of the 6,000 weight available per minute, shared with anything else on that IP.

After the backfill, poll on a schedule and overlap each window slightly. The overlap is harmless because the ledger ignores IDs it has already stored. Binance's limits are explained in more depth in [Binance API rate limits](https://fincodelabs.com/en/blog/binance-api-rate-limit).

## An append-only ledger: the core data model

Store every movement as an immutable row with a venue-scoped ID, and correct mistakes by adding rows instead of editing old ones.

**Minimum ledger fields**

| Field | Notes |
| --- | --- |
| `event_id` | Venue plus native ID, under a unique constraint. On Coinbase, `entry_id` identifies a fill, while `trade_id` is documented as not unique for adjusted fills |
| `account`, `time` | Exchange account or sub-account, and time in UTC |
| `kind` | Buy, sell, fee, deposit, withdrawal, reward, adjustment. Kraken's ledger types include `trade`, `deposit`, `withdrawal`, `staking` and `adjustment` |
| `asset`, `quantity` | Exact decimals, stored as text or a decimal type, never floats |
| `value`, `value_source` | Value in reporting currency and where the price came from |
| `raw_ref` | Pointer to the stored raw response |

Pair transfers between your own accounts by asset, amount and time, and keep the network fee as a separate row. Valuing history needs a price source, and redistribution terms apply if the numbers are shown to others; see [crypto price APIs](https://fincodelabs.com/en/blog/crypto-price-api).

## Average-cost calculation in Python

Average cost adds each purchase and its fee to a pool, then removes cost in proportion when units leave.

This runs on a synthetic ledger with a made-up asset, and shows a replayed row being ignored, an own transfer leaving cost unchanged and one reconciliation failure:

```python
from decimal import Decimal as D

# Synthetic normalized ledger, oldest first. "XYZ" is a made-up asset.
# (event id, venue, kind, quantity, gross value, fee in quote currency)
ledger = [
    ("A:t1", "A", "buy", "1.0", "100", "1.0"),
    ("A:t2", "A", "buy", "1.0", "200", "2.0"),
    ("A:t2", "A", "buy", "1.0", "200", "2.0"),  # re-fetched page, same ID
    ("A:t3", "A", "sell", "0.5", "150", "1.5"),
    ("A:w1", "A", "transfer_out", "0.5", "0", "0"),
    ("B:d1", "B", "transfer_in", "0.5", "0", "0"),
    ("A:w1f", "A", "network_fee", "0.01", "0", "0"),
]

seen, qty, cost, realized, balances = set(), D(0), D(0), D(0), {}
for eid, venue, kind, q, value, fee in ledger:
    if eid in seen:
        continue
    seen.add(eid)
    q, value, fee = D(q), D(value), D(fee)
    sign = 1 if kind in ("buy", "transfer_in") else -1
    balances[venue] = balances.get(venue, D(0)) + sign * q
    if kind == "buy":
        qty, cost = qty + q, cost + value + fee
    elif kind in ("sell", "network_fee"):
        removed = cost / qty * q
        qty, cost = qty - q, cost - removed
        if kind == "sell":
            realized += value - fee - removed
    # own transfers move units between venues, not cost

print("quantity", qty, "cost", round(cost, 3), "average", round(cost / qty, 2))
print("realized", round(realized, 2))
reported = {"A": D("0.99"), "B": D("0.48")}          # from balance endpoints
for venue, computed in sorted(balances.items()):
    gap = reported[venue] - computed
    print(venue, "computed", computed, "reported", reported[venue],
          "OK" if gap == 0 else f"EXCEPTION {gap}")
```

Output:

```
quantity 1.49 cost 225.735 average 151.50
realized 72.75
A computed 0.99 reported 0.99 OK
B computed 0.5 reported 0.48 EXCEPTION -0.02
```

The arithmetic checks by hand: (100 + 1 + 200 + 2) / 2 = 151.50, and the sale realizes 150 − 1.5 − 75.75 = 72.75. Treating a network fee as a disposal is one modeling choice, and here its 1.515 of cost leaves the pool without reaching the realized figure, so report it on its own line. A sale before any buy stops the sketch with a division error on the empty pool, so load units from outside as opening rows with their cost. Tax rules decide the method for filing, and they differ: HMRC's manual applies same-day and 30-day matching first and pools the rest of a token type in a section 104 pool, while the IRS FAQ, as read on 4 November 2026, applies first in, first out unless specific units are identified, and [Revenue Procedure 2024-28](https://www.irs.gov/pub/irs-drop/rp-24-28.pdf) explains that from 1 January 2025 Treasury Regulation 1.1012-1(j) applies those rules within each wallet or account. Keep the method configurable, label it on every report and have foreign readers check their home-country treatment. Turkish rules are covered in [Turkey crypto tax](https://fincodelabs.com/en/blog/turkey-crypto-tax).

## Crypto reconciliation software: proving the numbers

Reconciliation compares the balance computed from the ledger with the balance the exchange reports, per account and asset, and treats any gap as an open item.

Gaps usually have ordinary causes: a transfer still in flight, a reward or small-balance conversion the connector does not fetch, a delisted asset, or a product outside the tracker's scope. In the sample above venue B shows 0.02 less than computed, which should open an exception rather than a silent correction. Once the cause is known, add a labeled adjustment row. Report the as-of time and the last successful sync per venue, and mark a total as provisional while exceptions are open. Finance teams will ask for this before they trust any other number in crypto reporting software.

## Tracking and reporting are not portfolio management

Reading data and reporting it is software; deciding or trading for someone else is a regulated or prohibited activity in many places.

MiCA, Article 3(1)(25), defines portfolio management of crypto-assets as managing portfolios under mandates given by clients on a discretionary, client-by-client basis, and Article 3(1)(24) defines advice as personalized recommendations. In Turkey, [Communiqué III-35/B.2](https://www.resmigazete.gov.tr/eskiler/2025/03/20250313-6.htm), Article 15(3), prohibits managing customer portfolios on each customer's behalf for direct or indirect benefit, as a proxy or on oral authority with the same effect. That text sits in the platform rules, and whether it reaches a software company is a question for a lawyer; the practical answer is to stay well clear of it.

So the wording and features matter. Call the product a tracker or reporting tool. Avoid buy or sell prompts, rebalancing instructions and any promise of results; alerts should state facts such as a failed sync or a balance gap.

Rules depend on your jurisdiction and each platform's terms. For readers in Turkey, Article 99/A of Capital Markets Law No. 6362 treats activity by foreign platforms directed at Turkish residents as unauthorized, and the Capital Markets Board says appearing on its [operating list](https://spk.gov.tr/kurumlar/kripto-varlik-hizmet-saglayicilar/faaliyette-bulunanlar-listesi) does not mean a firm is authorized.

Fincode is a software company and does not provide investment advice, portfolio management or crypto asset services. Read-only reporting is the kind of work described on the [exchange API integration](https://fincodelabs.com/en/services/exchange-api-integration) page and in the [crypto portfolio risk dashboard](https://fincodelabs.com/en/projects/crypto-portfolio-risk-dashboard) scenario.

## Frequently asked questions

### Is there a free API for a crypto portfolio tracker?

Exchanges generally do not charge for reading your own account through their APIs, within rate limits. Historical prices for valuation may need a paid data plan, depending on the provider and your use.

### Can a read-only API key move my funds?

A key with read permission only cannot place orders or withdraw. It still reveals balances and trades, so keep it secret, restrict its IP and verify its permissions where the exchange allows.

### Is a crypto portfolio tracker app better than Excel?

A spreadsheet works for a few accounts and manual imports. Once you need automatic sync, reconciliation or an audit trail, a small service with a ledger is easier to trust.

### What does it take to develop crypto asset management software?

For tracking and reporting: connectors, a ledger, cost calculation and reconciliation, as described here. Software that decides or places trades for others is a different product with licensing questions.

## Sources

- [Binance Spot API documentation: REST API (account, myTrades, limits)](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md)
- [Binance Wallet API: Capital (deposit and withdraw history)](https://developers.binance.com/en/docs/catalog/core-trading-wallet/api/rest-api/capital)
- [Binance Wallet API: Get API key permission](https://developers.binance.com/en/docs/catalog/core-trading-wallet/api/rest-api/account)
- [Coinbase Advanced Trade API: List Fills](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/list-fills)
- [Coinbase Advanced Trade API: Get API Key Permissions](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/data-api/get-api-key-permissions)
- [Coinbase App API: Transactions](https://docs.cdp.coinbase.com/coinbase-app/track-apis/transactions)
- [Kraken: API key permissions](https://docs.kraken.com/exchange/guides/rest/api-keys)
- [Kraken: Get API Key Info](https://docs.kraken.com/api-reference/account-data/get-api-key-info)
- [Kraken: Spot REST rate limits](https://docs.kraken.com/exchange/guides/rest/ratelimits)
- [Kraken: Get Trades History](https://docs.kraken.com/api-reference/account-data/get-trades-history)
- [Kraken: Get Ledgers Info](https://docs.kraken.com/api-reference/account-data/get-ledgers-info)
- [HMRC Cryptoassets Manual: CRYPTO22200, pooling](https://www.gov.uk/hmrc-internal-manuals/cryptoassets-manual/crypto22200)
- [IRS: Frequently asked questions on virtual currency transactions (Q39 to Q41)](https://www.irs.gov/individuals/international-taxpayers/frequently-asked-questions-on-virtual-currency-transactions)
- [ESMA interactive single rulebook: MiCA Article 3, definitions](https://www.esma.europa.eu/publications-and-data/interactive-single-rulebook/mica/article-3-definitions)
- [Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/crypto-portfolio-tracker-api

Cite as: Fincode, "Crypto portfolio tracker API: build on read-only keys", updated November 5, 2026, https://fincodelabs.com/en/blog/crypto-portfolio-tracker-api
