Read-only Solana DeFi positions with explicit coverage and sourced USD values. Give it a wallet
address; it returns the wallet's positions in the protocols it supports (staking, liquid staking, perpetuals,
lending and Raydium liquidity so far), every leg with an exact amount from chain state, a USD value only where
one can be sourced (null otherwise), and the coverage behind the answer.
What it is not. Totals cover the supported protocols only: they are not wallet net worth. It is not a price oracle and its prices are not executable quotes. It never asks for a wallet connection or a signature, and it never sends a transaction.
Quickstart
During the private alpha every /v1/* route except /v1/health needs an access token
(ask at <CONTACT_EMAIL>). The first request for a wallet usually returns
202 with a job to poll; once the scan finishes (typically a few seconds), the same request returns
200 with the snapshot.
BASE_URL="https://api.lucidblock.xyz"
W="G4VmLamEV6h15gndx7nHn8TTcu7fJkBBNVN3wTugMNtH"
AUTH="Authorization: Bearer $LUCIDBLOCK_TOKEN"
curl -s -H "$AUTH" "$BASE_URL/v1/solana/wallets/$W/defi-positions" # 202: {job_id, poll_url, retry_after_seconds}
curl -s -H "$AUTH" "$BASE_URL/v1/solana/wallets/$W/defi-positions/jobs/<job_id>" # poll until succeeded or partial
curl -s -H "$AUTH" "$BASE_URL/v1/solana/wallets/$W/defi-positions" # 200: the snapshot
Honour Retry-After and retry_after_seconds. Poll the job (cost 1), not the positions
(cost 5).
Endpoints
| Method and path | Purpose | Cost |
|---|---|---|
GET /v1/health |
Service health: ok, degraded or down. No token needed. |
1 |
GET /v1/solana/wallets/{wallet}/defi-positions |
Wallet snapshot (200) or a queued scan (202). Query: protocols,
include, include_rewards_in_total, freshness_seconds,
limit, cursor.
|
5 |
GET /v1/solana/wallets/{wallet}/defi-positions/jobs/{jobId} |
Scan job status: queued, running, succeeded,
partial, failed, expired.
|
1 |
GET /v1/solana/protocols |
Protocol adapters and their status (verified, awaiting review, quarantined, disabled). | 1 |
GET /v1/solana/protocols/candidates |
Programs seen but not supported (experimental; no wallet data). | 1 |
GET /v1/solana/protocols/{programId}/discovery |
Discovery detail for one program (experimental; no wallet data). | 1 |
GET /v1/solana/coverage |
Service-wide coverage: categories covered and the release status. | 1 |
GET /openapi.json |
The OpenAPI 3.1 contract. No token, no rate-limit cost. | 0 |
GET /assets/v1/{file} |
First-party token and protocol logo files referenced by metadata. No token, no rate-limit
cost.
|
0 |
GET / |
This page. No token, no rate-limit cost. | 0 |
Reading a snapshot
-
Coverage first.
coverage.statusiscomplete_for_supported_protocols,partialorstale. Each protocol incoverage.protocols[]iscomplete,partial,error,not_applicableorskipped(with anerror_code). The absence of a position means something only for protocols listedcomplete. -
Then totals.
totals.total_value_usdis non-null only when coverage is complete and every counted leg is priced; otherwise it isnullandtotals.total_null_reasonssays why.totals.known_value_usdis the sum of the legs we could price (priced plus indicative): it is not a total, and not a lower bound when a debt is unpriced. -
Then positions and legs. Each position has
assets,debts(positive amounts) andrewardslegs. Each leg carriesamount_raw(atomic integer) andamount(exact decimal),value_usd,pricing_source,valuation_status,in_totalsandexclusion_reason.
Null, not zero. Unknown is null, never "0". Amounts and USD values
are decimal strings: parse them with a decimal library, not floats.
Status vocabulary
Leg valuation_status
priced: valued from a qualified source; counts towardtotal_value_usd.-
indicative: thin market; inknown_value_usd, never intotal_value_usd. stale: the latest price is too old; shown, counted in neither total.-
unsupported: no supported price source for this token;value_usdisnull. -
no_liquidity: no market deep enough to price it;value_usdisnull. -
unlicensed: a price exists but cannot be served;value_usdisnull. error: pricing failed during the scan;value_usdisnull.-
not_requested: valuation was not requested (includewithoutvaluation).
Coverage statuses
-
Snapshot:
complete_for_supported_protocols(every supported protocol checked),partial(at least one could not be checked),stale(older than the freshness target; a refresh is running). -
Protocol:
complete,partial,error,not_applicable,skipped(seeerror_code).
owner_role
authority: the wallet owns the obligation, perp or position account.withdraw_authority: the wallet can withdraw this stake.-
stake_authority: the wallet can only delegate this stake (staker only); shown, never counted in totals. token_holder: the receipt or share token is in the wallet.beneficiary: the wallet is entitled through a farm or custody account.delegate: the wallet acts for the owner under a delegation.unknown: the relationship could not be classified.
Errors and limits
Every error has the same envelope, and every response carries X-Request-Id:
{ "error": { "code": "INVALID_ADDRESS", "message": "…", "details": { } }, "request_id": "…" }
| HTTP | Codes |
|---|---|
| 400 |
INVALID_PARAMETER, INVALID_ADDRESS, UNSUPPORTED_PROTOCOL,
CURSOR_EXPIRED
|
| 401 | UNAUTHORIZED (missing or wrong access token) |
| 404 / 405 | NOT_FOUND, JOB_NOT_FOUND, METHOD_NOT_ALLOWED |
| 429 |
RATE_LIMITED (details.reason: per_ip_rate or
cold_scan_limit)
|
| 500 | INTERNAL |
| 503 |
DB_UNAVAILABLE, QUEUE_UNAVAILABLE, SNAPSHOT_UNAVAILABLE,
SCAN_FAILED, RPC_BUDGET_EXHAUSTED, CONFIG_INVALID
|
Rate limits: 60 units per minute per IP. A wallet read costs 5 units; everything else under
/v1 costs 1. At most 20 new wallet scans per IP per hour (cold_scan_limit). Responses
carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; a 429
carries Retry-After. Retry only 429 and 503, after Retry-After.
Links
- OpenAPI 3.1 contract (
/openapi.json) - Service health (
/v1/health) - Demo viewer: app.lucidblock.xyz (paste a wallet address, see every leg and the raw API calls)
- Full developer docs: available on request at
<CONTACT_EMAIL>