Skip to main content

Query Cookbook

Every query on this page is executed against the live testnet endpoint in CI. If one stops working, the build fails.

Copy any of them into the explorer or send them with curl. Addresses in the examples are real testnet records, so they return data as written.

Set a User-Agent

The endpoint sits behind Cloudflare, which rejects default library user-agents with a bare 403 and no GraphQL error body. Always set your own. See Limits and gotchas.

curl -s https://babylon-vault-indexer-api.testnet.babylonlabs.io/ \
-H 'Content-Type: application/json' \
-H 'User-Agent: my-app/1.0' \
-d '{"query":"{ statss(limit:1){ items{ totalAvailableSats availableVaultCount } } }"}'

For depositors and borrowers

Find all my vaults

Filter by your depositor address. totalCount is the full match count, not the page size.

{
vaults(
where: { depositor: "0x106d71c740aeebf6e72f06dad4129651dc65d810" }
orderBy: "pendingAt"
orderDirection: "desc"
limit: 10
) {
totalCount
items {
id
status
amount
activatedAt
inUse
}
}
}

amount is in satoshis, returned as a string because it is a BigInt.

Inspect one vault

{
vault(id: "0x002f198c46664c28f0685ba86303256e9833344a54c7b4e34483e7805f5b8d4a") {
status
amount
depositor
vaultProvider
vaultProviderCommissionBps
peginTxHash
depositorPayoutBtcAddress
pendingAt
verifiedAt
activatedAt
expiredAt
expirationReason
}
}

A vault moves through pendingsignatures_collectedverifiedavailable, then to one of redeemed, liquidated, expired, depositor_withdrawn or invalid.

The timestamps above date the first four states and expired (signatures_collected is dated by peginSigsPostedAt, available by activatedAt). The vault record carries no timestamp for redeemed, liquidated, depositor_withdrawn or invalid. Date a redemption or a liquidation from vaultActivity instead, and treat status as the authority in every case.

Check my Aave position and its collateral

{
aavePosition(depositorAddress: "0xd51d8dda9f8975fbecacd77a93f37a57053642e1") {
totalCollateral
proxyContract
createdAt
updatedAt
collaterals(limit: 10) {
totalCount
items {
vaultId
amount
addedAt
removedAt
liquidationIndex
}
}
}
}

A removedAt of null means the collateral is still pledged.

Read my activity history

{
vaultActivitys(
where: { depositor: "0x106d71c740aeebf6e72f06dad4129651dc65d810" }
orderBy: "timestamp"
orderDirection: "desc"
limit: 20
) {
totalCount
items {
type
amount
vaultId
debtReserveId
timestamp
transactionHash
}
}
}

In practice type is one of deposit, borrow, repay, redeem, liquidation or claim_expired. The enum also declares withdrawal, add_collateral and remove_collateral, but nothing writes those, so they never appear. See Known data gaps.

Two things to handle when you render these rows:

  • vaultId is null on borrow and repay. Those events belong to an Aave position, not to one vault.
  • amount is in satoshis except on borrow and repay, where it is in the debt asset. Resolve debtReserveId against aaveReserve for the symbol and decimals before formatting.

For operators

Protocol totals

{
statss(limit: 1) {
items {
totalAvailableSats
availableVaultCount
updatedAt
}
}
protocolStates(limit: 1) {
items {
activeVaultCoreVersion
updatedAt
}
}
}

Registered vault providers

{
vaultProviders(orderBy: "registeredAt", orderDirection: "asc", limit: 50) {
totalCount
items {
id
name
commissionBps
metadataStatus
metadataRejectionReason
registeredAt
}
}
}

The security participant set

Vault keepers are per-application. Universal challengers are system-wide.

{
vaultKeepers(limit: 50) {
totalCount
items {
id
}
}
universalChallengers(limit: 50) {
totalCount
items {
id
}
}
universalChallengerVersions(orderBy: "id", orderDirection: "desc", limit: 5) {
items {
id
}
}
}

Vault count by status

The endpoint allows at most 15 aliases per operation, so keep status breakdowns to a handful of aliases per request.

{
pending: vaults(where: { status: pending }, limit: 1) {
totalCount
}
verified: vaults(where: { status: verified }, limit: 1) {
totalCount
}
available: vaults(where: { status: available }, limit: 1) {
totalCount
}
redeemed: vaults(where: { status: redeemed }, limit: 1) {
totalCount
}
expired: vaults(where: { status: expired }, limit: 1) {
totalCount
}
liquidated: vaults(where: { status: liquidated }, limit: 1) {
totalCount
}
}

Fee escrow state

{
escrowed: vaultFeeEscrows(where: { status: escrowed }, limit: 1) {
totalCount
}
distributed: vaultFeeEscrows(where: { status: distributed }, limit: 1) {
totalCount
}
refunded: vaultFeeEscrows(where: { status: refunded }, limit: 1) {
totalCount
}
forfeited: vaultFeeEscrows(where: { status: forfeited }, limit: 1) {
totalCount
}
}

Indexer sync status

_meta reports the last block the indexer processed. Compare it against a chain head to measure lag.

{
_meta {
status
}
}

For builders

Paginate past the 1000-row cap

limit is capped at 1000. Page with offset, and stop when you have collected totalCount rows.

{
vaults(orderBy: "pendingAt", orderDirection: "asc", limit: 1000, offset: 0) {
totalCount
items {
id
status
}
}
}

Ordering matters. Without a stable orderBy, rows can repeat or vanish between pages.

Filter operators

Which suffixed filters a field accepts depends on its type. Every field takes the exact value, _not, _in and _not_in. Beyond that:

Field typeExtra operators
BigInt, Int_gt, _gte, _lt, _lte
String_contains, _starts_with, _ends_with, each with a _not_ form
Boolean, enumsnone

Strings get no ordering operators — depositor_gt is not a field, and asking for it fails validation. Combine filters with AND and OR.

{
vaults(
where: {
AND: [
{ status: available }
{ amount_gte: "1000000" }
{ inUse: true }
]
}
orderBy: "amount"
orderDirection: "desc"
limit: 5
) {
totalCount
items {
id
amount
inUse
}
}
}

BigInt comparisons take strings, not numbers.

Follow a relation

Some entities expose the related record directly, which avoids a second round trip.

{
vaultFeeEscrows(limit: 3, orderBy: "escrowedAt", orderDirection: "desc") {
items {
vaultId
totalAmount
status
numUniversalChallengers
numAppVaultKeepers
vault {
status
depositor
}
}
}
}

Keep an eye on nesting. The endpoint rejects any operation deeper than 10 levels.

Introspect within the depth cap

The standard introspection query that GraphiQL and most tooling sends is depth 21, and this endpoint rejects it. This shorter query lists every type and is depth 4.

{
__schema {
types {
kind
name
}
}
}

To read one type's fields without tripping the cap:

{
__type(name: "vault") {
name
fields {
name
type {
kind
name
ofType {
kind
name
}
}
}
}
}

Four such queries reconstruct the entire schema. The docs site ships the result already assembled at /schema/vault-indexer.graphql, so most builders never need to introspect at all.