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.
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 pending → signatures_collected → verified →
available, 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:
vaultIdisnullonborrowandrepay. Those events belong to an Aave position, not to one vault.amountis in satoshis except onborrowandrepay, where it is in the debt asset. ResolvedebtReserveIdagainstaaveReservefor 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 type | Extra operators |
|---|---|
BigInt, Int | _gt, _gte, _lt, _lte |
String | _contains, _starts_with, _ends_with, each with a _not_ form |
Boolean, enums | none |
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.