Metadata-Version: 2.4
Name: auradefi
Version: 0.1.2
Summary: Open-source multi-tenant crypto data aggregator: Vezgo-style tenancy, DeBank-style DeFi depth, Plaid wire format. Library first, service second.
Project-URL: Homepage, https://github.com/auracarehq/auradefi
Project-URL: Documentation, https://auradefi.info/
Project-URL: Source, https://github.com/auracarehq/auradefi
Project-URL: Issues, https://github.com/auracarehq/auradefi/issues
Author-email: Auracare <stephen@auracare.org.uk>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: aggregator,blockchain,caip,caip-19,crypto,defi,multi-tenant,plaid,portfolio,wallet
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Provides-Extra: api
Requires-Dist: fastapi>=0.115; extra == 'api'
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: fastapi>=0.115; extra == 'dev'
Requires-Dist: ipykernel; extra == 'dev'
Requires-Dist: nbclient; extra == 'dev'
Requires-Dist: nbformat; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: sqlmodel>=0.0.16; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Provides-Extra: docs
Requires-Dist: markdown-it-py>=3; extra == 'docs'
Requires-Dist: pygments>=2.17; extra == 'docs'
Provides-Extra: sql
Requires-Dist: sqlmodel>=0.0.16; extra == 'sql'
Description-Content-Type: text/markdown

# auradefi

Open-source, multi-tenant crypto data aggregator. It takes the tenancy model
from Vezgo, the DeFi position depth from DeBank and the transaction
decomposition from Zerion, then emits Plaid's wire format, so crypto lands in
the same downstream schema as bank and exchange data.

It is a library first and a service second. A Python host imports `auradefi`
directly and pays no serialisation or network cost. The HTTP API is a thin
shell over the same importable core.

> Status: alpha, and 0.1.2 is the release to use. It is 3,426 tests green
> offline on a fresh clone with no API keys, its notebooks execute clean, and
> every example under [`examples/`](examples) runs against the published
> wheel. Work towards 0.2.0 is on `main` ahead of that release: the EVM node
> path landed first (a JSON-RPC client, a Multicall3 batcher, a log scanner
> and a concrete on-chain reader), so this page describes a tree that is not
> yet a release. [`STATUS.md`](docs/internal/STATUS.md) carries the live gate
> state, including which gates are red today.
>
> Do not use 0.1.0. A separate adversarial review of it found nineteen
> verified defects, of which five were security and four were silent data
> loss. None of them failed a test.
> [`docs/internal/RELEASE_0.1.1.md`](docs/internal/RELEASE_0.1.1.md) is the
> full accounting. 0.1.1 fixes all nineteen and deliberately breaks one id
> derivation, so read *Upgrading* in [`CHANGELOG.md`](CHANGELOG.md) before you
> move library-ingested data across.
>
> Alpha means the gaps in *What is not there* are real; read that section
> before you budget work against this.
> [`STATUS.md`](docs/internal/STATUS.md) carries the live gate state, and
> [`docs/internal/SPEC.md`](docs/internal/SPEC.md) is the design contract.

**[Documentation site →](https://auradefi.info/)** carries the
examples, the thirteen executable notebooks and the full reference, rendered
with every example's real output.

## Install

```bash
pip install auradefi                # core; httpx is the only dependency
pip install 'auradefi[sql]'         # + the SQLModel ledger backend
pip install 'auradefi[api]'         # + the FastAPI HTTP surface
```

From a clone (if your system python has no pip, `scripts/bootstrap.sh`
handles it):

```bash
git clone https://github.com/auracarehq/auradefi
cd auradefi && bash scripts/bootstrap.sh
.venv/bin/pytest                           # the whole suite, offline, no keys
.venv/bin/python examples/quickstart.py    # every capability, end to end
bash scripts/run_examples.sh               # all thirteen examples
```

## Examples

[`examples/`](examples) holds one file per question. Each one is
self-contained, reads nothing from this repository, and runs offline without
API keys. Each asserts its own output, and CI executes all of them, so a
stale example fails the build.

| Example | What it answers |
|---|---|
| [`quickstart.py`](examples/quickstart.py) | the whole library in one file; start here |
| [`01_holdings_for_an_address.py`](examples/01_holdings_for_an_address.py) | a priced portfolio, exactly, with unpriced assets named |
| [`02_embed_in_your_backend.py`](examples/02_embed_in_your_backend.py) | your ports, your tick, your database, and restart resume |
| [`03_write_a_source_adapter.py`](examples/03_write_a_source_adapter.py) | point it at your own chain data (two methods) |
| [`04_persist_to_your_database.py`](examples/04_persist_to_your_database.py) | host-owned DDL, a resumable cursor feed, a reorg |
| [`05_serve_the_http_api.py`](examples/05_serve_the_http_api.py) | Plaid's envelope over HTTP, and batch partial success |
| [`06_isolate_two_tenants.py`](examples/06_isolate_two_tenants.py) | one deployment, many customers, attacked four ways |
| [`07_read_defi_positions.py`](examples/07_read_defi_positions.py) | an LP and a loan that still add up to net worth |
| [`08_report_cost_basis_and_pnl.py`](examples/08_report_cost_basis_and_pnl.py) | FIFO/LIFO/HIFO/ACB, any instant, Plaid `tax_lots[]` |
| [`09_deliver_signed_webhooks.py`](examples/09_deliver_signed_webhooks.py) | signing, the pinned retry schedule, replay |
| [`10_scan_bitcoin_and_solana.py`](examples/10_scan_bitcoin_and_solana.py) | an xpub that never leaves the process, and Token-2022 |
| [`11_provoke_every_error.py`](examples/11_provoke_every_error.py) | every error on purpose, so you can test your handler |
| [`12_read_a_contract_from_a_node.py`](examples/12_read_a_contract_from_a_node.py) | eth_call, a batch, five reads in one round trip, and logs |

[`examples/README.md`](examples/README.md) annotates the index, and the
[documentation site](https://auradefi.info/examples/) renders
every example with its real output.

## Using it

As a library, where the host owns storage, transport, prices and the tick:

```python
from auradefi import Auradefi

auradefi = Auradefi(
    ledger=SqlModelLedger(session_factory=my_session_factory),  # your database
    source=MySource(),        # your transport: .balances() + .fetch_txlist()
    prices=MyPrices(),        # your price feed: .usd_prices()
)
user = auradefi.user("opaque-host-user-id")   # get-or-create, id is derived
user.connect_address("eip155:1", "0x…")       # validated now, not on a later tick
report = auradefi.sync(budget=5)              # budgeted, resumable, self-throttling
holdings, metrics = auradefi.holdings(), auradefi.scalar_metrics()
```

As a service, where `create_app` takes ports you already built:

```python
from auradefi.api.app import create_app
from auradefi.api.deps import Deps

app = create_app(Deps(tenancy=…, keys=…, ledger=…, webhooks=…, clock=…))
# POST /auth/token, POST /connections, GET /crypto/sync  (Plaid's envelope)
# GET /coverage, POST /webhooks/endpoints, POST /webhooks/…/replay
```

[`docs/books/`](docs/books) walks both surfaces executably and offline.

## What works today

Coverage is published as data (rule #10). Every row below has an executable
notebook under [`docs/books/`](docs/books) that runs offline and asserts its
own outputs, plus a gate test under `tests/`.

| Capability | Limits, and what proves it |
|---|---|
| `Quantity`/`Money`: exact at 10^77, four-field wire form, `raw` always a JSON string, strict wire grammar | [`02_money`](docs/books/02_money.ipynb) |
| CAIP-2/CAIP-19 parse + canonicalize, deterministic `ast_…` ids, both-ways asset registry | 5 seed chains (Ethereum, Polygon, Base, Bitcoin, Solana); [`03_assets_chains`](docs/books/03_assets_chains.ipynb) |
| Asset groups (decimals-equality law, `single` fallback) + additive spam scoring (score + numbers, caller threshold) | [`03_assets_chains`](docs/books/03_assets_chains.ipynb) |
| Ledger port: idempotent upsert, cursor sync with `has_more` paging, reorg as `removed` + re-`added`, resurrection, tenant isolation | memory and SQLModel backends; [`04_ledger`](docs/books/04_ledger.ipynb) |
| Cassette replay harness (`CassetteMissError` offline guarantee) | [`01_foundation`](docs/books/01_foundation.ipynb) |
| Style gates: size, structure, placement, layering (`tests/style`) | no allowlist |
| EVM balances to holdings, exact-`Decimal` USD totals, unpriced assets named | Etherscan V2 source + DefiLlama prices; [`05_holdings`](docs/books/05_holdings.ipynb) |
| Tenancy: org/project/end-user, scoped `adk_` keys, `authEndpoint` JWT mint, three-window quota, audit log | the isolation gate actively tries to leak; [`06_tenancy`](docs/books/06_tenancy.ipynb) |
| Rich transactions: `parts[]`/`acts[]`, fees as siblings carrying `borne_by`, derived `type`, ledger bridge, reorg + resurrection | EVM only, one act per transaction; [`07_transactions`](docs/books/07_transactions.ipynb) |
| DeFi positions: adapter protocol, drill-down, group totals + health factor, signed synthetic-Holdings projection | Uniswap v2/v3, Aave v3, Lido/Rocket Pool; [`08_positions`](docs/books/08_positions.ipynb) |
| EVM node path: `keccak256`, a static ABI codec, JSON-RPC single calls and id-matched batches, Multicall3 `aggregate3` with per-call failure declared, chunked `eth_getLogs`, and `EvmContractReader` as the adapters' seam | Ethereum-shaped chains, static ABI types only, no async; [`13_evm_node`](docs/books/13_evm_node.ipynb) |
| Embedding: `from auradefi import Auradefi`, host-owned session, budgeted two-phase sync, 26-metric scalar projection | chain-scoped connection ids, restart resume enumerated from the state port, one connection's failure contained to its own row (0.1.1 #18/#21/#24/#26); [`09_embedding`](docs/books/09_embedding.ipynb), [`02_embed_in_your_backend.py`](examples/02_embed_in_your_backend.py) |
| Bitcoin: pure-Python BIP32 xpub derivation, gap-20 scan, confirmed-only UTXO balances | p2wpkh + Esplora only; the extended key never reaches HTTP; [`10_bitcoin_solana`](docs/books/10_bitcoin_solana.ipynb) |
| Solana: SPL + Token-2022 balances, ScaledUiAmount carried both ways, signature history | balances only, no decode; [`10_bitcoin_solana`](docs/books/10_bitcoin_solana.ipynb) |
| HTTP API: Plaid `/crypto/sync` envelope, connections, `/coverage` generated as data, nine quota headers, batch holdings | [`12_http_api`](docs/books/12_http_api.ipynb) |
| Webhooks: HMAC-SHA256 signed, durable over a pinned retry schedule, dead letter + replay | [`12_http_api`](docs/books/12_http_api.ipynb) |
| Accounting: lot ledger, FIFO/LIFO/HIFO/ACB, realised + unrealised PnL, arbitrary-date PnL, Plaid `tax_lots` | 50,000-event gate; [`11_accounting`](docs/books/11_accounting.ipynb) |

### What is not there

Rule #10 applies to the absences too.

- There are no live network adapters beyond what the cassettes cover. Every
  I/O path is exercised against committed recordings. Pointing a source at the
  real Etherscan, Esplora or Solana RPC needs your own keys and endpoints,
  and CI has not reconciled the output against an incumbent.
- The EVM node path has never met a real node. `EvmContractReader`,
  `EvmRpc`, `Multicall3` and `scan_logs` ship and the five position adapters
  resolve through them, but every vector that proves it is a hand-authored
  cassette: the block-20,450,000 words were packed from the integers the
  existing goldens pin, not recorded from an archive node, and
  `tests/golden/test_phase11_reader.py` says so in its own docstring. What is
  proven is the selector derivation, the word packing, the tuple decode, the
  block pin and the JSON-RPC path end to end. Agreement with mainnet state at
  that block is not proven by anything here.
- The codec is static types only: `uint<N>`, `int<N>`, `address` and `bool`,
  plus Multicall3's own two dynamic shapes as named special cases. No
  `string`, no `bytes`, no arrays and no nested tuples, and it raises on them
  instead of guessing. Every function the shipped adapters call fits, and a
  contract of yours that does not will be refused rather than mis-encoded.
- Nothing in the package constructs an `EvmContractReader` for you. It is
  wired by a host, and the block a `ResolveContext` names is not threaded
  into the reader: the reader carries its own pin, chosen at construction, so
  a host that wants a report at block N builds the reader at block N.
- One price oracle (DefiLlama), current prices only. There is no fallback
  feed and no historical price service: `prices/historian.py` and
  `prices/store.py` are declared in the module layout and absent, as are the
  `coingecko`, `manual` and `onchain_amm` oracles, so accounting marks are
  the caller's.
- No scheduler and no background worker. The host owns every tick, and a
  backfill is a `sync()` budget you spend yourself. This is the design, not
  a gap: 0.2.0 removed `jobs/` from the declared layout after finding four
  of its five modules already shipped under other names, and the fifth was
  a cron evaluator wearing a domain's clothes. What is genuinely missing is
  a reprocess path for re-decoding stored rows after the decoder improves;
  the `decoder_version` it would select on is already persisted.
- Five of the nine declared `api/routes/` modules are absent:
  `accounts.py`, `holdings.py`, `positions.py`, `transactions.py` and
  `webhooks.py`. What ships is `auth`, `connections`, `sync` and `admin`, so
  holdings and positions have no HTTP surface of their own, and the webhook
  admin routes live in `admin.py`.
- `project/` ships only `scalar.py`. `project/plaid.py` and
  `project/native.py` are declared and absent. The Plaid envelope is
  projected in `api/wire.py` instead, so that projection is not reusable
  outside the HTTP shell the way the layer contract intends.
- Two ledger backends: in-memory and SQLModel/sqlite. Postgres should work
  through the same port; only sqlite is exercised. The tenancy, keys, quota,
  audit and webhook stores are in-memory only.
- Cosmos is absent, as is every EVM chain the registry does not seed. So are
  exchange connections, NFTs and protocol-specific decoders (`acts[]` is
  always one act, and `protocol` is always `None`).
- Solana transaction decode is not implemented. Balances and signature
  history only.
- No async surface, no background worker and no scheduler. The host owns the
  tick.
- There is no migration for the 0.1.1 embed id break. Library-ingested embed
  connection ids, and every `transaction_id` hashed over them, re-derive in
  0.1.1, so 0.1.0 rows written through `Auradefi` stop matching. A host
  either re-derives them itself or accepts the old rows as orphaned history
  (`CHANGELOG.md`, *Upgrading*). Data written through the HTTP API is
  unaffected.
- `SyncStatePort` is a five-method Protocol in 0.1.1 (`tenants()` was added).
  A host store written against the 0.1.0 four-method shape is refused at bind
  time, so it cannot silently sync nothing.

## The rules the code lives by

- Money is a tagged decimal string, and a raw amount is never a JSON integer.
- Asset ids are deterministic CAIP-19 and permanently stable.
- Every movement is a `part[]`; fees are siblings of movements.
- Multi-tenancy is designed in. Two tenants can never see each other's data.
- `pytest` passes on a fresh clone with no API keys, because the cassettes
  are committed.
- Files cap at 400 lines with no allowlist, and `tests/style/` enforces the
  layer contract.

## Docker

```bash
docker compose run --rm test    # full offline suite in a network-less container
docker compose run --rm demo    # quickstart against the installed wheel
```

## Docs

**[auradefi.info](https://auradefi.info/)** is
built from this repository, with every example executed at build time and
every signature generated from the code.

Start here:

- [Quickstart](https://auradefi.info/quickstart.html): five
  lines, no credentials
- [Authentication & keys](https://auradefi.info/authentication.html):
  what you need before mainnet (at most one key, and it is optional)
- [Bring your own](https://auradefi.info/bring-your-own.html):
  your API, your database, your prices, with every port and its methods
- [Guides](https://auradefi.info/examples/index.html):
  [`examples/`](examples), thirteen single files that run offline
- [API reference](https://auradefi.info/reference/index.html):
  signatures, parameters, return fields, exceptions
- [HTTP API](https://auradefi.info/http.html): Plaid's wire
  format, plus `openapi.json`
- [Errors](https://auradefi.info/errors.html): every
  exception and its HTTP status
- [Build with an LLM](https://auradefi.info/llms.html): a
  prompt to paste into a model, plus
  [llms.txt](https://auradefi.info/llms.txt) and the whole
  documentation as [one file](https://auradefi.info/llms-full.txt)

Also in the repository:

- [`docs/books/`](docs/books) holds thirteen executable notebooks, run
  headlessly in CI.
- [`CHANGELOG.md`](CHANGELOG.md) records what changed per release, including
  the 0.1.1 upgrade note.
- [`docs/internal/`](docs/internal) covers how this was designed and built
  rather than how to use it: the [design contract](docs/internal/SPEC.md),
  the [pinned algorithms](docs/internal/DECISIONS.md),
  [build status](docs/internal/STATUS.md), the
  [0.1.0 defect accounting](docs/internal/RELEASE_0.1.1.md), the
  [release procedure](docs/internal/RELEASING.md) and the
  [agent loop](docs/internal/AGENT_PROMPTS.md) that wrote most of it.

## Licence

Apache-2.0. See [`LICENSE`](LICENSE).
