> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vanna.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# Mercury Indexer

> How to query historical contract events from Vanna Protocol using the Mercury indexer — deposits, borrows, repayments, and liquidations.

Soroban RPC's `getEvents` endpoint has limited retention (a few ledgers). For historical event data — transaction history, borrow records, liquidation logs — Vanna uses [Mercury](https://mercurydata.app), an off-chain Stellar event indexer.

***

## Mercury Overview

Mercury subscribes to on-chain contract events and stores them in a queryable database. Two query paths:

| Path             | Use case                                            |
| ---------------- | --------------------------------------------------- |
| **GraphQL**      | Complex queries, aggregations, multi-contract joins |
| **REST Classic** | Simple event streams by contract and topic          |

<Note>
  Mercury's GraphQL endpoint on testnet is intermittently unreliable. Use the REST Classic events endpoint for production reliability.
</Note>

***

## Authentication

Mercury requires a JWT token. **Never expose this token to browser clients.** Use a server-side proxy (Next.js API route, Edge function, etc.):

```
Client → /api/mercury/events → Mercury REST API
                              (JWT added server-side)
```

**Required environment variables (server-side only):**

```bash theme={null}
MERCURY_URL=https://api.mercurydata.app    # or testnet endpoint
MERCURY_KEY=your_jwt_token_here
```

***

## REST Events Endpoint

### Server-Side Proxy (Next.js)

```typescript theme={null}
// app/api/mercury/events/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const contract = searchParams.get('contract');
  const limit = searchParams.get('limit') ?? '100';
  const cursor = searchParams.get('cursor');
  const account = searchParams.get('account'); // optional: filter by account

  if (!contract) {
    return NextResponse.json({ error: 'contract required' }, { status: 400 });
  }

  // Build Mercury URL
  let url = `${process.env.MERCURY_URL}/zephyr/execute`;
  // For REST Classic:
  url = `${process.env.MERCURY_URL}/rest/events/by-contract/${contract}?limit=${limit}`;
  if (cursor) url += `&cursor=${cursor}`;

  const response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${process.env.MERCURY_KEY}`,
      'Content-Type': 'application/json',
    },
  });

  const data = await response.json();
  return NextResponse.json(data);
}
```

### Client-Side Fetch

```typescript theme={null}
interface MercuryEvent {
  id: string;
  contract_id: string;
  topic1: string;   // event name (base64 XDR)
  topic2: string;   // first arg (e.g., account address)
  topic3: string;
  topic4: string;
  data: string;     // event payload (base64 XDR)
  tx: string;       // transaction hash
  ledger: number;
  ledger_closed_at: string;
}

async function fetchContractEvents(
  contract: string,
  options: { limit?: number; cursor?: string; account?: string } = {},
): Promise<MercuryEvent[]> {
  const params = new URLSearchParams({
    contract,
    limit: String(options.limit ?? 100),
    ...(options.cursor ? { cursor: options.cursor } : {}),
    ...(options.account ? { account: options.account } : {}),
  });

  const response = await fetch(`/api/mercury/events?${params}`);
  if (!response.ok) throw new Error('Mercury fetch failed');
  return response.json();
}
```

***

## Event Schemas

### AccountManager Events

**Account Creation:**

```
topic1: "AccountCreationEvent"
data: { smart_account: Address, creation_time: u64 }
```

**Trader Borrow:**

```
topic1: "TraderBorrowEvent"
topic2: <smart_account_address>
data: { smart_account: Address, token_symbol: Symbol, token_amount: U256 }
```

**Trader Repay:**

```
topic1: "TraderRepayEvent"
topic2: <smart_account_address>
data: { smart_account: Address, token_amount: U256, timestamp: u64, token_symbol: Symbol }
```

**Liquidation:**

```
topic1: "TraderLiquidateEvent"
topic2: <smart_account_address>
data: { smart_account: Address, timestamp: u64 }
```

### LendingPool Events

**Deposit:**

```
topic1: "deposit_event"
topic2: <lender_address>
data: { lender: Address, amount: U256, timestamp: u64, asset_symbol: Symbol }
```

**Withdraw:**

```
topic1: "withdraw_event"
topic2: <lender_address>
data: { lender: Address, vtoken_amount: U256, timestamp: u64, asset_symbol: Symbol }
```

**vToken Mint:**

```
topic1: "mint_event"
data: { lender: Address, token_amount: U256, timestamp: u64, token_symbol: Symbol }
```

**vToken Burn:**

```
topic1: "burn_event"
data: { lender: Address, token_amount: U256, timestamp: u64, token_symbol: Symbol }
```

***

## Decoding Event Data

Mercury returns topic and data fields as base64-encoded XDR `ScVal`. Decode them with the Stellar SDK:

```typescript theme={null}
import { xdr, scValToNative } from '@stellar/stellar-sdk';

function decodeScVal(base64: string): unknown {
  const buffer = Buffer.from(base64, 'base64');
  const scVal = xdr.ScVal.fromXDR(buffer);
  return scValToNative(scVal);
}

// Example: decode a borrow event
const event = events[0];
const eventName = decodeScVal(event.topic1) as string;  // "TraderBorrowEvent"
const smartAccount = decodeScVal(event.topic2) as string;
const payload = decodeScVal(event.data) as {
  smart_account: string;
  token_symbol: string;
  token_amount: bigint;
};
```

***

## Cursor-Based Pagination

Mercury returns events newest-first (descending ID). Use the last event's `id` as the cursor for the next page:

```typescript theme={null}
async function fetchAllEvents(contract: string): Promise<MercuryEvent[]> {
  const all: MercuryEvent[] = [];
  let cursor: string | undefined;

  while (true) {
    const page = await fetchContractEvents(contract, { limit: 100, cursor });
    all.push(...page);

    if (page.length < 100) break;              // last page
    cursor = page[page.length - 1].id;         // continue from last event
  }

  return all;
}
```

***

## Filtering by Account

Mercury supports topic-based filtering. To fetch only events for a specific margin account, pass the account address as `account` — the proxy encodes it to XDR and forwards as a `topics=` filter:

```typescript theme={null}
// Fetch all borrow events for a specific smart account
const events = await fetchContractEvents(CONTRACTS.ACCOUNT_MANAGER, {
  account: smartAccountAddress,
  limit: 50,
});
```

***

## Enriching with Ledger Timestamps

Mercury provides `ledger_closed_at` in ISO format for each event. If you need Unix timestamps or need to join with Horizon data:

```typescript theme={null}
import { Horizon } from '@stellar/stellar-sdk';

const horizon = new Horizon.Server('https://horizon-testnet.stellar.org');

// Enrich events with exact close time from Horizon
async function enrichWithTimestamp(event: MercuryEvent) {
  const ledger = await horizon.ledgers().ledger(event.ledger).call();
  return {
    ...event,
    closedAt: new Date(ledger.closed_at),
    closedAtUnix: Math.floor(new Date(ledger.closed_at).getTime() / 1000),
  };
}
```

***

## Practical Examples

### Fetch a User's Borrow History

```typescript theme={null}
async function getUserBorrowHistory(smartAccount: string) {
  const events = await fetchContractEvents(CONTRACTS.ACCOUNT_MANAGER, {
    account: smartAccount,
  });

  return events
    .filter(e => decodeScVal(e.topic1) === 'TraderBorrowEvent')
    .map(e => {
      const payload = decodeScVal(e.data) as {
        token_symbol: string;
        token_amount: bigint;
      };
      return {
        txHash: e.tx,
        ledger: e.ledger,
        closedAt: e.ledger_closed_at,
        symbol: payload.token_symbol,
        amountWad: payload.token_amount,
        amountHuman: Number(payload.token_amount) / 1e18,
      };
    });
}
```

### Fetch All Liquidations

```typescript theme={null}
async function getAllLiquidations() {
  const events = await fetchContractEvents(CONTRACTS.ACCOUNT_MANAGER);

  return events
    .filter(e => decodeScVal(e.topic1) === 'TraderLiquidateEvent')
    .map(e => {
      const payload = decodeScVal(e.data) as {
        smart_account: string;
        timestamp: bigint;
      };
      return {
        smartAccount: payload.smart_account,
        timestamp: Number(payload.timestamp),
        txHash: e.tx,
        ledger: e.ledger,
      };
    });
}
```

### Monitor Pool Deposits in Real Time

Combine Mercury for history with Horizon SSE for live updates:

```typescript theme={null}
// Historical: Mercury
const history = await fetchContractEvents(CONTRACTS.POOL_XLM, { limit: 200 });

// Live: Horizon ledger-close SSE
const horizon = new Horizon.Server('https://horizon-testnet.stellar.org');
horizon
  .ledgers()
  .cursor('now')
  .stream({
    onmessage: (ledger) => {
      // Invalidate your query cache on each new ledger
      queryClient.invalidateQueries(['pool-deposits']);
    },
  });
```

***

## Mercury Pricing

| Plan    | Network | Price      |
| ------- | ------- | ---------- |
| Free    | Testnet | Free       |
| Builder | Mainnet | \$79/month |

Sign up at [mercurydata.app](https://mercurydata.app).

***

## Related

* [Events & Indexing Guide](/developers/guides/events-indexing) — complete integration walkthrough
* [Transaction Flow](/developers/sdk/transaction-flow) — submitting transactions
* [Getting Started](/developers/sdk/getting-started) — SDK setup
