This document provides realistic, ready-to-run GraphQL examples for the three
main families of endpoints exposed by the Stellar Analytics API
(http://localhost:4000/graphql):
- Dashboard — the widgets that power the public dashboard (network stats, ledger activity, top accounts, asset volumes, daily transaction counts).
- Account — per-account analytics (overview, transaction history, operations filtered by type).
- Network analytics — bulk aggregations used by researchers, dashboards and back-office tooling.
Every example below lists:
- What it solves — a one-line description of the realistic use case.
- Query — the GraphQL document, copy-paste ready.
- Variables — inline, ready to send.
- Expected response — a realistic, illustrative JSON response so you know what shape to expect.
- cURL — a copy-paste shell command that hits a local API.
All example accounts below are realistic Stellar public-key addresses in StrKey / G… form (
Gfollowed by exactly 55 base32 characters, 56 characters total). They parse correctly through the API and theed25519decoder used by the sharedstellartypes, without being tied to any real on-chain account. The ledger, transaction and operation shapes mirror the values the API returns against a fully-populated indexer database.
| Item | Convention |
|---|---|
| HTTP method | POST for any request with a body (queries, mutations) |
| Content-Type | application/json |
| Endpoint | POST /graphql with { "query": "...", "variables": { ... } } |
| Variable names | Match the API arg names: $limit / $cursor for ledgers, $limit for topAccounts, etc. The naming-convention lint rule in graphql-query-standards.md enforces camelCase |
| Pagination | Cursor-based via limit + cursor on the ledgers connection. Other list queries (transactions, operations, topAccounts, dailyTransactionCount) are limit-only today (see §2.2) |
| Limits | Always pass limit when fetching list endpoints — defaults are unconstrained on some resolvers and may return thousands of rows |
| Address format | Stellar public key, G + 55 base32 chars, 56 chars total — see shared/src/config/networks.ts |
The args the API distinguishes today:
| Argument | Required? | Why |
|---|---|---|
ledger(sequence: Int!) |
Required | You must point at a single ledger |
accountStats(address: String!) |
Required | Account-only — no "default" |
assetVolume(assetCode: String!, timeframe: String!) |
Both required | Must specify what and how-far-back |
ledgers(limit, cursor) |
Both optional | Defaults to latest 10 |
transactions(address, limit) |
Both optional | Browses global or per-account |
operations(type, limit) |
Both optional | Browses all types or one |
topAccounts(limit) |
Optional | Defaults to top 10 |
dailyTransactionCount(days) |
Optional | Defaults to 7 |
networkStats |
n/a | No args |
Practical rule: any !-decorated argument in the schema will reject the
query at validation time, so prefer sending them up-front in the client.
These power the home page of the dashboard: the "live" status panel, the 24h KPI tiles, the daily transaction chart and the leaderboard.
What it solves. Render the four-up KPI tile at the top of the home page showing the current transactions-per-second, total accounts, last-24h active accounts and total ledgers ingested.
Query.
query GetNetworkKpis {
networkStats {
tps
totalAccounts
activeAccounts24h
totalLedgers
}
}Expected response.
{
"data": {
"networkStats": {
"tps": 12.42,
"totalAccounts": 1834502,
"activeAccounts24h": 84137,
"totalLedgers": 54120882
}
}
}cURL.
curl -s http://localhost:4000/graphql \
-H 'content-type: application/json' \
-d '{"query":"query { networkStats { tps totalAccounts activeAccounts24h totalLedgers } }"}' \
| jq .💡 Tip. Call this query on page load and refresh every ~10s — the
tpsandactiveAccounts24hfields change frequently while the totals move slowly. Avoid caching.
What it solves. Populate the "latest ledgers" feed on the dashboard with cursor-based pagination so the user can scroll back through history without calling a different endpoint.
Query.
query GetLedgerFeed($limit: Int, $cursor: String) {
ledgers(limit: $limit, cursor: $cursor) {
edges {
cursor
node {
sequence
hash
closeTime
transactionCount
}
}
pageInfo {
hasNextPage
endCursor
}
}
}Variable names track the API arg names exactly (
$limit,$cursor) so client tooling that auto-generates types stays in sync. TheGet…naming prefix is the lint convention fromgraphql-query-standards.md.
Variables (first page — no cursor).
{
"limit": 20,
"cursor": null
}Expected response.
{
"data": {
"ledgers": {
"edges": [
{
"cursor": "NTQxMjA4ODE=",
"node": {
"sequence": 54120881,
"hash": "e7a3c89f0b1d4a26c5e9f7b8a2d14f3c9b6e5a4f8e7d3c2b1a0f9e8d7c6b5a49f",
"closeTime": "2025-07-26T12:14:09.000Z",
"transactionCount": 84
}
},
{
"cursor": "NTQxMjA4ODA=",
"node": {
"sequence": 54120880,
"hash": "4b1ce28ac39d4e7f9a1b3c5d7e9f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
"closeTime": "2025-07-26T12:14:02.000Z",
"transactionCount": 71
}
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "NTQxMjA4NjE="
}
}
}
}The cursor returned on each edge is base64 of the integer
sequence as ASCII: e.g. base64("54120881") === "NTQxMjA4ODE=".
Hand endCursor back in as the cursor variable of your next call to
walk backwards through history.
cURL.
curl -s http://localhost:4000/graphql \
-H 'content-type: application/json' \
-d '{
"query": "query GetLedgerFeed($limit: Int, $cursor: String){ ledgers(limit: $limit, cursor: $cursor){ edges { cursor node { sequence hash closeTime transactionCount } } pageInfo { hasNextPage endCursor } } }",
"variables": { "limit": 20, "cursor": null }
}' | jq .💡 Tip. Treat
endCursoras opaque — don't decode it in the client. WhenpageInfo.hasNextPageisfalseyou've reached the earliest ledger the indexer has stored.
What it solves. Render the bar chart on the dashboard showing the number of transactions per day for the last 30 days.
Query.
query GetDailyTransactionChart($days: Int) {
dailyTransactionCount(days: $days) {
date
count
}
}Variables.
{
"days": 30
}Expected response.
{
"data": {
"dailyTransactionCount": [
{ "date": "2025-07-26", "count": 1045217 },
{ "date": "2025-07-25", "count": 982004 },
{ "date": "2025-07-24", "count": 971562 },
{ "date": "2025-07-23", "count": 885213 }
]
}
}💡 Tip. The result is ordered newest → oldest. Frontend charts can reverse it once before rendering. Keep
days ≤ 90to stay within documented performance limits — seegraphql-query-limits.md.
What it solves. Populate the "Top accounts" table on the dashboard.
⚠️ balanceis a placeholder — please read. ThetopAccountsresolver currently surfaces transaction count under thebalancefield name because the indexer does not yet store native balances. Treat the value as activity until native balance ingestion ships. Renaming the resolver or adding a separateactivityfield are both being evaluated — seedocs/query-performance.md.
Query.
query GetTopAccounts($limit: Int) {
topAccounts(limit: $limit) {
address
balance
}
}Variables.
{
"limit": 10
}Expected response (illustrative — balance shows activity).
{
"data": {
"topAccounts": [
{
"address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
"balance": 9821043
},
{
"address": "GB7JKDHY43FLK3EWJQ4H7RYJZPLQXE3JTLL4DCE4T5PDBN3JZBQ6KQSV",
"balance": 7703412
}
]
}
}What it solves. Show the rolling volume for a single asset code on the asset detail page.
Query.
query GetAssetVolume($assetCode: String!, $timeframe: String!) {
assetVolume(assetCode: $assetCode, timeframe: $timeframe) {
assetCode
volume
timeframe
}
}Variables.
{
"assetCode": "USDC",
"timeframe": "24h"
}Expected response.
{
"data": {
"assetVolume": {
"assetCode": "USDC",
"volume": "184722994102",
"timeframe": "24h"
}
}
}💡 Tip.
timeframeis one of24h,7d,30d. Treat anything else as"24h"on the client and warn — the API currently falls back silently.
These answer "tell me about this Stellar account" questions. All of them
take a Stellar public key (the G… 56-char form) as input.
What it solves. When the user lands on an account page, show the header strip: transaction count, total payment volume and last-active timestamp.
Query.
query GetAccountOverview($address: String!) {
accountStats(address: $address) {
address
transactionCount
totalPaymentVolume
lastActive
}
}Variables.
{
"address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
}Expected response.
{
"data": {
"accountStats": {
"address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
"transactionCount": 4218,
"totalPaymentVolume": "182010492104",
"lastActive": "2025-07-26T11:58:42.000Z"
}
}
}cURL.
curl -s http://localhost:4000/graphql \
-H 'content-type: application/json' \
-d '{
"query": "query GetAccountOverview($address: String!){ accountStats(address: $address){ address transactionCount totalPaymentVolume lastActive } }",
"variables": { "address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF" }
}' | jq .What it solves. Populate the "Transactions" tab on the account detail page — the most recent first, with their parent ledger sequence so the dashboard can deep-link back to a ledger.
Query.
query GetAccountTransactions($address: String!, $limit: Int) {
transactions(address: $address, limit: $limit) {
hash
ledgerSequence
sourceAccount
feeCharged
operations {
id
type
sourceAccount
createdAt
}
}
}Variables.
{
"address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
"limit": 25
}Expected response.
{
"data": {
"transactions": [
{
"hash": "a3f1c9d4e5b6f7a8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"ledgerSequence": 54120881,
"sourceAccount": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
"feeCharged": "100",
"operations": [
{
"id": "a3f1c9d4e5b6f7a8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2-0",
"type": "payment",
"sourceAccount": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
"createdAt": "2025-07-26T12:14:09.870Z"
}
]
}
]
}
}
⚠️ Pagination roadmap.transactionscurrently supports only limit-based pagination. Cursor-based pagination is tracked for parity withledgers— see the operations changelog.
What it solves. Filter the "Operations" tab to show only the operations
of a specific kind — e.g. all payment operations that involved this
account or all createAccount operations globally for a date range.
Query.
query GetOperationsByType($type: String!, $limit: Int) {
operations(type: $type, limit: $limit) {
id
txHash
type
sourceAccount
createdAt
}
}Variables.
{
"type": "payment",
"limit": 50
}Expected response.
{
"data": {
"operations": [
{
"id": "a3f1c9d4e5b6f7a8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2-0",
"txHash": "a3f1c9d4e5b6f7a8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"type": "payment",
"sourceAccount": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
"createdAt": "2025-07-26T12:14:09.870Z"
},
{
"id": "b7c2d3a4f5e6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2-1",
"txHash": "b7c2d3a4f5e6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2",
"type": "payment",
"sourceAccount": "GB7JKDHY43FLK3EWJQ4H7RYJZPLQXE3JTLL4DCE4T5PDBN3JZBQ6KQSV",
"createdAt": "2025-07-26T12:13:55.420Z"
}
]
}
}💡 Tip. Combine
operations(type: "createAccount")with thedailyTransactionCountaggregation to build a "new accounts per day" panel. Today you'll need to post-filter in the client; see the roadmap note above about extendingdailyTransactionCountto take an operation type.
These are the bulk aggregations used by analysts, the home dashboard, and back-office / data-science jobs.
What it solves. When a user clicks a row in the ledger feed, deep-link them to the ledger detail page showing all transactions and operations in that ledger — fetched in a single round trip via the resolvers' nested fields and DataLoader batching.
Query.
query GetLedgerDetail($sequence: Int!) {
ledger(sequence: $sequence) {
sequence
hash
closeTime
transactionCount
transactions {
hash
sourceAccount
feeCharged
operations {
id
type
sourceAccount
}
}
operations {
id
txHash
type
sourceAccount
createdAt
}
}
}Variables.
{
"sequence": 54120881
}Expected response.
{
"data": {
"ledger": {
"sequence": 54120881,
"hash": "e7a3c89f0b1d4a26c5e9f7b8a2d14f3c9b6e5a4f8e7d3c2b1a0f9e8d7c6b5a49f",
"closeTime": "2025-07-26T12:14:09.000Z",
"transactionCount": 84,
"transactions": [
{
"hash": "a3f1c9d4e5b6f7a8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"sourceAccount": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
"feeCharged": "100",
"operations": [
{
"id": "a3f1c9d4e5b6f7a8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2-0",
"type": "payment",
"sourceAccount": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
}
]
}
],
"operations": [
{
"id": "a3f1c9d4e5b6f7a8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2-0",
"txHash": "a3f1c9d4e5b6f7a8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"type": "payment",
"sourceAccount": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
"createdAt": "2025-07-26T12:14:09.870Z"
}
]
}
}
}💡 Performance. Nested selections here are batched by the
operationsByTxHashandtransactionsByLedgerSeqDataLoaders — the shape above results in 3 SQL queries, not N+1. Seedocs/query-performance.mdfor the full batching story.
What it solves. Render a leaderboard of asset volumes for the last
week. Combine multiple assetVolume requests into a single document to
share one round trip, since each query is small and independent.
Query.
query GetAssetVolumeBoard {
usdc24h: assetVolume(assetCode: "USDC", timeframe: "24h") {
assetCode
volume
timeframe
}
usdc7d: assetVolume(assetCode: "USDC", timeframe: "7d") {
assetCode
volume
timeframe
}
xlm7d: assetVolume(assetCode: "XLM", timeframe: "7d") {
assetCode
volume
timeframe
}
btc7d: assetVolume(assetCode: "BTC", timeframe: "7d") {
assetCode
volume
timeframe
}
}Note: the aliases
usdc24h:,usdc7d:, etc. are GraphQL aliases that keep each result distinct underdatadespite the same field name.
Expected response.
{
"data": {
"usdc24h": { "assetCode": "USDC", "volume": "184722994102", "timeframe": "24h" },
"usdc7d": { "assetCode": "USDC", "volume": "1035210884723", "timeframe": "7d" },
"xlm7d": { "assetCode": "XLM", "volume": "9852310044", "timeframe": "7d" },
"btc7d": { "assetCode": "BTC", "volume": "41250321", "timeframe": "7d" }
}
}The dashboard hero panel renders a network KPI tile, a 5-row snapshot of the latest ledger feed, and a 7-day chart. All three can be merged into a single GraphQL document with aliased fields to avoid three HTTP round trips on every page load.
Query.
query GetDashboardHero($limit: Int, $days: Int) {
stats: networkStats {
tps
totalAccounts
activeAccounts24h
totalLedgers
}
recent: ledgers(limit: $limit) {
edges {
cursor
node {
sequence
closeTime
transactionCount
}
}
}
chart: dailyTransactionCount(days: $days) {
date
count
}
}Variables.
{
"limit": 5,
"days": 7
}Expected response (shape only).
{
"data": {
"stats": {
"tps": 12.42,
"totalAccounts": 1834502,
"activeAccounts24h": 84137,
"totalLedgers": 54120882
},
"recent": {
"edges": [
{ "cursor": "NTQxMjA4ODE=", "node": { "sequence": 54120881, "closeTime": "2025-07-26T12:14:09.000Z", "transactionCount": 84 } }
]
},
"chart": [
{ "date": "2025-07-26", "count": 1045217 },
{ "date": "2025-07-25", "count": 982004 }
]
}
}The hero query sits at depth 3 — well under the depth-10 limit. See §5.2 below for the kind of query that does hit the limit.
All of the queries above can fail. The standard GraphQL error envelope is
returned alongside the data field. Common cases:
| Cause | extensions.code |
Client behaviour |
|---|---|---|
| Query rejected by depth/complexity limit | GRAPHQL_VALIDATION_FAILED |
Refuse and retry with a slimmer query — see graphql-query-limits.md |
| Database unavailable | INTERNAL_SERVER_ERROR |
Show a graceful banner; do not retry immediately |
| Account has no transactions yet | (no error, returns transactionCount: 0 and totalPaymentVolume: "0") |
Render the empty state |
sequence / address malformed |
GRAPHQL_VALIDATION_FAILED |
Client-side validation should catch this first |
| Rate-limited (HTTP 429 outside of GraphQL envelope) | Too many requests… |
Back off per IP — see docs/cors.md |
{
"errors": [
{
"message": "'GetLedgerDetail' exceeds maximum operation depth of 10",
"extensions": { "code": "GRAPHQL_VALIDATION_FAILED" }
}
],
"data": null
}graphql-depth-limit counts the longest path of nested object
selections, not the number of opening braces in the query string.
Scalars (e.g. Int, String) and aliases on the same level do not add
depth. With the current schema the deepest realistic path is
ledger (depth 1)
→ transactions (depth 2)
→ operations (depth 3)
— three selection-sets deep — so the depth-10 limit is essentially
unreachable in practice against valid queries. The realistic lever
against wide attacks is query complexity (graphql-query-complexity)
rather than depth; see graphql-query-limits.md.
Implementation status. The depth-limit rule is not currently wired into the deployed API server (
api/src/index.tsdoes not applyvalidationRules, andgraphql-depth-limitis not inapi/package.json). The depth value configured ingraphql-query-limits.mdis aspirational — a follow-up issue should land the rule invalidationRules. TheGRAPHQL_VALIDATION_FAILEDenvelope below is the documented shape the rule will emit once wired:
{
"errors": [
{
"message": "'GetLedgerDetail' exceeds maximum operation depth of 10",
"extensions": { "code": "GRAPHQL_VALIDATION_FAILED" }
}
],
"data": null
}Even though the depth limit is hard to hit on this schema, the same query can still exhaust the database by fanning out. The following "God query" selects large lists in a single round trip and triggers expensive joins via the nested resolvers:
query GetEverything {
stats: networkStats { tps totalAccounts activeAccounts24h totalLedgers }
leaders: topAccounts(limit: 100) { address balance } # one COUNT GROUP BY over transactions
recent: ledgers(limit: 100) {
edges {
cursor
node {
sequence
transactionCount
transactions # joins via transactionsByLedgerSeq loader
operations # joins via operationsByLedgerSeq loader
}
}
}
chart: dailyTransactionCount(days: 90) { date count }
}This passes depth-limit (max path depth here is 4) and passes the naming/standards linter, but it is expensive enough to need explicit deferral — under load it can hold DB workers for hundreds of ms to several seconds depending on database state and concurrency. Practical lesson: fetch progressively
- Hero query first (§4) for the page load;
- Ledger feed on demand (§1.2);
- Single ledger detail on click (§3.1).
The DataLoaders in api/src/loaders.ts only really save you on
intra-ledger fetches — they do not protect the indexer from
unbounded list sizes. See
docs/query-performance.md for the full
batching story.
A throwaway playground is served at GET /graphql when
NODE_ENV !== "production" — it ships a single example query and a
run button. Useful for smoke-testing the examples above.
Browsers do not accept the
POST /graphqlrequest fromGET /graphqldirectly when the GraphQL request body is large; prefer the cURL examples above for scripted testing.
graphql-query-standards.md— naming rules and lint configuration enforced on every query document.graphql-query-limits.md— depth and cost limits applied at the API layer.query-performance.md— DataLoader batching, slow-query monitoring and index maintenance.error-handling-and-logging.md— standardized error shapes and log fields used across the API.api/src/schema.ts— the canonical GraphQL schema source-of-truth.api/src/resolvers/index.ts— the resolver implementations backing each of the queries above.