Agent Skills: Stellar Data: RPC + Horizon

Querying Stellar chain data via Stellar RPC (preferred) and Horizon (legacy). Covers RPC JSON-RPC methods, Horizon REST endpoints, streaming, pagination, historical queries, Hubble/Galexie for deep history, and the RPC/Horizon migration story. Use when reading balances, transactions, operations, ledgers, contract events, or building any indexer/analytics workflow.

UncategorizedID: stellar/stellar-dev-skill/data

Install this agent skill to your local

pnpm dlx add-skill https://github.com/stellar/stellar-dev-skill/tree/HEAD/skills/data

Skill Files

Browse the full folder contents for data.

Download Skill

Loading file tree…

skills/data/SKILL.md

Skill Metadata

Name
data
Description
Querying Stellar chain data via Stellar RPC (preferred) and Horizon (legacy). Covers RPC JSON-RPC methods, Horizon REST endpoints, streaming, pagination, historical queries, Hubble/Galexie for deep history, and the RPC/Horizon migration story. Use when reading balances, transactions, operations, ledgers, contract events, or building any indexer/analytics workflow.

Stellar Data: RPC + Horizon

API access for reading chain state. Stellar RPC is the preferred entry point for new projects; Horizon remains for legacy and historical-query workflows. For deeper history beyond RPC's 7-day window, use Hubble/Galexie.

When to use this skill

  • Calling Stellar RPC methods (getLatestLedger, getLedgerEntries, getEvents, simulateTransaction, sendTransaction)
  • Querying Horizon endpoints (accounts, transactions, operations, effects, ledgers)
  • Streaming live events or operations
  • Pulling historical data beyond RPC's 7-day window (Hubble, Galexie)
  • Choosing between RPC and Horizon for a given workflow

Related skills

  • Building transactions to send → ../dapp/SKILL.md
  • Smart contract simulation and event emission → ../smart-contracts/SKILL.md
  • Asset balance and trustline lookups → ../assets/SKILL.md
  • Standards (SEP-7 deeplinks, SEP-10 auth) → ../standards/SKILL.md

Overview

Stellar provides two API paradigms:

| API | Status | Use Case | |-----|--------|----------| | Stellar RPC | Preferred | Smart contracts, real-time state, new projects | | Horizon | Legacy-focused | Historical data, legacy applications |

Recommendation: Use Stellar RPC for all new projects. Use Horizon mainly for historical queries and legacy compatibility paths.

Read the file that matches the task

| Task | File | |------|------| | RPC methods and usage | Stellar RPC (below) | | Horizon endpoints, common operations, streaming, pagination | horizon.md | | Migration strategy | Migration: Horizon to RPC (below) | | Data history/indexing options | Historical Data Access (below) | | Environment setup and endpoints | Network Configuration (below) |

Stellar RPC

Endpoints

Note: SDF directly provides Futurenet public RPC. For Mainnet RPC, select a provider from the RPC providers directory.

| Network | RPC URL | |---------|---------| | Mainnet | Provider-specific endpoint (see RPC providers directory) | | Testnet | https://soroban-testnet.stellar.org | | Futurenet | https://rpc-futurenet.stellar.org | | Local | http://localhost:8000/soroban/rpc |

Setup

import * as StellarSdk from "@stellar/stellar-sdk";

const rpc = new StellarSdk.rpc.Server("https://soroban-testnet.stellar.org");

Key Methods

Get Account

const account = await rpc.getAccount(publicKey);
// Returns account with sequence number for transaction building

Get Health

const health = await rpc.getHealth();
// { status: "healthy" }

Get Latest Ledger

const ledger = await rpc.getLatestLedger();
// { id: "...", sequence: 123456, protocolVersion: 25 }

Get Ledger Entries

// Read contract storage
const key = StellarSdk.xdr.LedgerKey.contractData(
  new StellarSdk.xdr.LedgerKeyContractData({
    contract: new StellarSdk.Address(contractId).toScAddress(),
    key: StellarSdk.xdr.ScVal.scvSymbol("Counter"),
    durability: StellarSdk.xdr.ContractDataDurability.persistent(),
  })
);

const entries = await rpc.getLedgerEntries(key);
if (entries.entries.length > 0) {
  const value = StellarSdk.scValToNative(
    entries.entries[0].val.contractData().val()
  );
}

Simulate Transaction

const simulation = await rpc.simulateTransaction(transaction);

if (StellarSdk.rpc.Api.isSimulationError(simulation)) {
  console.error("Simulation failed:", simulation.error);
} else if (StellarSdk.rpc.Api.isSimulationSuccess(simulation)) {
  console.log("Cost:", simulation.cost);
  console.log("Result:", simulation.result);
}

Send Transaction

const response = await rpc.sendTransaction(signedTransaction);

if (response.status === "PENDING") {
  // Poll for result
  let result = await rpc.getTransaction(response.hash);
  while (result.status === "NOT_FOUND") {
    await new Promise(r => setTimeout(r, 1000));
    result = await rpc.getTransaction(response.hash);
  }

  if (result.status === "SUCCESS") {
    console.log("Success:", result.returnValue);
  } else {
    console.error("Failed:", result.status);
  }
}

Get Transaction

const tx = await rpc.getTransaction(txHash);
// status: "SUCCESS" | "FAILED" | "NOT_FOUND"
// returnValue: ScVal (for contract calls)
// ledger: number

Get Events

const events = await rpc.getEvents({
  startLedger: 1000000,
  filters: [
    {
      type: "contract",
      contractIds: [contractId],
      topics: [
        ["*", StellarSdk.xdr.ScVal.scvSymbol("transfer").toXDR("base64")],
      ],
    },
  ],
});

for (const event of events.events) {
  console.log("Event:", event.topic, event.value);
}

RPC Limitations

  • 7-day history for most methods: getTransaction, getEvents, etc. only cover recent data
  • getLedgers exception: on a data-lake-backed provider, "Infinite Scroll" pages back past the retention window — as far as that provider's data lake reaches (potentially genesis). On a plain RPC instance it is bounded by getHealth().oldestLedger; requests older than that fail with -32600. Check before assuming depth.
  • No streaming: Poll for updates (no WebSocket)
  • Contract-focused: Limited classic Stellar data

Migration: Horizon to RPC

Account Loading

// Horizon (old)
const account = await horizonServer.loadAccount(publicKey);

// RPC (new)
const account = await rpc.getAccount(publicKey);
// Note: RPC returns less data, just what's needed for transactions

Transaction Submission

// Horizon (for classic transactions)
const result = await horizonServer.submitTransaction(tx);

// RPC (for smart contract transactions)
const response = await rpc.sendTransaction(tx);
const result = await pollForResult(response.hash);

Historical Data

// Horizon - full history
const allTxs = await horizonServer
  .transactions()
  .forAccount(publicKey)
  .call();

// RPC - most methods limited to the retention window (~7 days)
// Exception: getLedgers can page further back (Infinite Scroll), but only as far
// as the chosen provider's retention or data-lake integration reaches.
// Always check the floor of the instance you're talking to first:
const { oldestLedger } = await rpc.getHealth();
// For guaranteed full history, use:
// 1. Hubble (SDF's BigQuery dataset)
// 2. Galexie (data pipeline)
// 3. Your own indexer

Streaming Replacement

// Horizon - native streaming
server.payments().stream({ onmessage: handlePayment });

// RPC - polling (no native streaming)
async function pollForUpdates() {
  const lastLedger = await rpc.getLatestLedger();
  // Check for new events/transactions
  // Repeat on interval
}
setInterval(pollForUpdates, 5000);

Historical Data Access

For data older than the RPC retention window (~7 days — not available via most RPC methods; getLedgers reaches further only on data-lake-backed providers, see Data Lake below):

Hubble (BigQuery)

-- Query Stellar data in BigQuery
SELECT *
FROM `crypto-stellar.crypto_stellar.history_transactions`
WHERE source_account = 'G...'
ORDER BY created_at DESC
LIMIT 100

Galexie

Self-hosted data pipeline for processing Stellar ledger data:

  • https://github.com/stellar/galexie

Data Lake

RPC "Infinite Scroll" is powered by the Stellar data lake — a cloud-based object store (SEP-0054 format). Deep getLedgers history is a property of the provider, not the method: an instance only serves history past its retention window if its operator wired a data lake in. Instances without one (the public SDF testnet RPC included) reject older start ledgers with JSON-RPC -32600 — compare your target against getHealth().oldestLedger before paging back.

  • Public access: s3://aws-public-blockchain/v1.1/stellar/ledgers/pubnet (AWS Open Data)
  • Self-host: Use Galexie to export to AWS S3 or Google Cloud Storage
  • Hosted: Quasar (Lightsail Network) provides hosted Galexie Data Lake + Archive RPC endpoints
  • Size: ~3.8TB, growing ~0.5TB/year
  • Cost: ~$160/month self-hosted ($60 compute + $100 storage)
  • Docs: https://developers.stellar.org/docs/data/apis/rpc/admin-guide/data-lake-integration

Third-Party Indexers

For complex queries, event streaming, or custom data pipelines beyond what RPC/Horizon provide:

  • Mercury — Stellar-native indexer with Retroshades, GraphQL API (https://mercurydata.app)
  • SubQuery — Multi-chain indexer with Stellar support, event handlers (https://subquery.network)
  • Goldsky — Real-time data replication pipelines and subgraphs (https://goldsky.com)
  • StellarExpert API — Free, no-auth REST API for assets, accounts, ledger resolution (https://stellar.expert/openapi.html)

See the full indexer directory: https://developers.stellar.org/docs/data/indexers

Network Configuration

For a React/Next.js-specific setup, see the dapp skill. For mainnet RPC, set STELLAR_MAINNET_RPC_URL from a provider in the RPC providers directory.

Environment-Based Setup

// lib/stellar-config.ts
import * as StellarSdk from "@stellar/stellar-sdk";

type NetworkConfig = {
  rpcUrl: string;
  horizonUrl: string;
  networkPassphrase: string;
  friendbotUrl: string | null;
};

const requireEnv = (name: string): string => {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required env var: ${name}`);
  return value;
};

// Lazy per-network factories: requireEnv only runs for the selected network,
// so testnet/local work without the mainnet env var set.
const configs: Record<string, () => NetworkConfig> = {
  mainnet: () => ({
    rpcUrl: requireEnv("STELLAR_MAINNET_RPC_URL"),
    horizonUrl: "https://horizon.stellar.org",
    networkPassphrase: StellarSdk.Networks.PUBLIC,
    friendbotUrl: null,
  }),
  testnet: () => ({
    rpcUrl: "https://soroban-testnet.stellar.org",
    horizonUrl: "https://horizon-testnet.stellar.org",
    networkPassphrase: StellarSdk.Networks.TESTNET,
    friendbotUrl: "https://friendbot.stellar.org",
  }),
  local: () => ({
    rpcUrl: "http://localhost:8000/soroban/rpc",
    horizonUrl: "http://localhost:8000",
    networkPassphrase: "Standalone Network ; February 2017",
    friendbotUrl: "http://localhost:8000/friendbot",
  }),
};

const network = process.env.STELLAR_NETWORK || "testnet";
const makeConfig = configs[network];
if (!makeConfig) throw new Error(`Unknown network: ${network}`);
export const config = makeConfig();

export const rpc = new StellarSdk.rpc.Server(config.rpcUrl);
export const horizon = new StellarSdk.Horizon.Server(config.horizonUrl);

Best Practices

Use RPC for:

  • New application development
  • Smart contract interactions
  • Transaction simulation and submission
  • Real-time account state

Use Horizon for:

  • Historical transaction queries
  • Payment streaming
  • Legacy application maintenance
  • Rich account metadata

Error Handling

// RPC errors
try {
  const result = await rpc.sendTransaction(tx);
} catch (error) {
  if (error.code === 400) {
    // Invalid transaction
  } else if (error.code === 503) {
    // Service unavailable
  }
}

// Horizon errors
try {
  const result = await horizon.submitTransaction(tx);
} catch (error) {
  const extras = error.response?.data?.extras;
  if (extras?.result_codes) {
    // Detailed error codes
    console.log("Transaction:", extras.result_codes.transaction);
    console.log("Operations:", extras.result_codes.operations);
  }
}

Rate Limiting

Both RPC and Horizon have rate limits:

  • Use exponential backoff for retries
  • Cache responses where appropriate
  • Consider running your own nodes for high-volume applications
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  let lastError: Error;
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      if (error.response?.status === 429) {
        // Rate limited - exponential backoff
        await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
      } else {
        throw error;
      }
    }
  }
  throw lastError;
}