Skip to main content
This page provides a quick reference for the Portal API. For the complete OpenAPI specification with all fields and options:

Complete Solana API Specification

View the full OpenAPI documentation with all available fields, filters, and detailed schemas →

Base URL

All Portal requests are made to:
https://portal.sqd.dev/datasets/{network}
Replace {network} with your target blockchain (e.g., solana-mainnet).

Core Endpoints

POST /stream

Stream blockchain data with filters. Includes unfinalized blocks.

POST /finalized-stream

Stream only finalized blocks. Use for production pipelines requiring certainty.

GET /metadata

Get dataset information: slot range, finality rules, and network details.

Installation

npm install @subsquid/portal-client

Quick Start Examples

curl --compressed -X POST 'https://portal.sqd.dev/datasets/solana-mainnet/stream' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "solana",
    "fromBlock": 259984950,
    "toBlock": 259985000,
    "fields": {
      "block": { "number": true, "timestamp": true },
      "instruction": { "programId": true, "accounts": true, "data": true }
    }
  }'

Available Fields

Block Fields

Select which block header fields to retrieve:
fields: {
  block: {
    number: true,          // Slot number (field name: number)
    hash: true,            // Block hash
    parentNumber: true,    // Parent slot number
    parentHash: true,      // Parent block hash
    height: true,          // Block height
    timestamp: true,       // Unix timestamp
  }
}

Transaction Fields

Select transaction data fields:
fields: {
  transaction: {
    signatures: true,      // Transaction signatures (base58)
    feePayer: true,        // Fee payer address
    err: true,             // Transaction error (null if successful)
    version: true,         // Transaction version
    accountKeys: true,     // Account keys
    recentBlockhash: true, // Recent blockhash
    fee: true,             // Transaction fee
    computeUnitsConsumed: true, // Compute units consumed
  }
}

Instruction Fields

Select instruction data fields:
fields: {
  instruction: {
    programId: true,       // Program address
    accounts: true,        // Account addresses
    data: true,            // Instruction data (base58)
    transactionIndex: true, // Transaction index (use to match with transactions)
    instructionAddress: true,  // Instruction address in call tree
    isCommitted: true,     // Whether instruction succeeded
  }
}

Log Fields

Select log message fields:
fields: {
  log: {
    programId: true,       // Program address
    message: true,         // Log message
    kind: true,            // Log kind (log, data, other)
    transactionIndex: true, // Transaction index (use to match with transactions)
    instructionAddress: true,  // Instruction address
  }
}

Token Balance Fields

Select token balance fields:
fields: {
  tokenBalance: {
    account: true,         // Token account address
    preMint: true,         // Mint before transaction
    postMint: true,        // Mint after transaction
    preAmount: true,       // Balance before transaction
    postAmount: true,      // Balance after transaction
    preOwner: true,        // Owner before transaction
    postOwner: true,       // Owner after transaction
  }
}

Balance Fields

Select SOL balance fields:
fields: {
  balance: {
    account: true,         // Account address
    pre: true,             // Balance before transaction
    post: true,            // Balance after transaction
  }
}

Reward Fields

Select reward fields:
fields: {
  reward: {
    pubkey: true,          // Reward recipient
    lamports: true,        // Reward amount
    rewardType: true,      // Reward type
    postBalance: true,     // Balance after reward
  }
}

Filtering Options

Instruction Filters

Filter instructions by program, discriminator, or accounts:
instructions: [
  {
    programId: ["whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"], // Orca Whirlpool
    d8: ["0xf8c69e91e17587c8"], // Swap instruction discriminator
    accounts: ["7qbRF6YsyGuLUVs6Y1q64bdVrfe4ZcUUz1JRdoVNUJnm"], // Specific pool
  },
];
Filter logic:
  • Multiple program IDs in one object = OR
  • Multiple discriminators in one object = OR
  • Multiple filter objects = OR

Transaction Filters

Filter transactions by fee payer:
transactions: [
  {
    feePayer: ["YourWalletAddressHere"], // Fee payer addresses
  },
];

Log Filters

Filter logs by program and kind:
logs: [
  {
    programId: ["whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"],
    kind: ["log"], // log, data, or other
  },
];

Token Balance Filters

Filter token balances by mint or account:
tokenBalances: [
  {
    preMint: ["EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"], // USDC
    account: ["TokenAccountAddress..."],
  },
];

Network Selection

Specify the Solana network when creating the data source:
// Solana mainnet
const solana = new DataSource({ network: "solana-mainnet" });

View All Networks

See complete list of supported Solana networks

Query Patterns

Pattern 1: Range Query

Query a specific slot range:
const blocks = await dataSource.getBlocks({
  from: 259984950,
  to: 260000000,
  fields: {
    /* fields */
  },
  instructions: [
    /* filters */
  ],
});

Pattern 2: Batch Processing

Process large ranges in batches:
const BATCH_SIZE = 10000;

for (let i = startSlot; i < endSlot; i += BATCH_SIZE) {
  const blocks = await dataSource.getBlocks({
    from: i,
    to: Math.min(i + BATCH_SIZE, endSlot),
    fields: {
      /* fields */
    },
  });

  // Process blocks
  await processBlocks(blocks);
}

Pattern 3: Multi-Program Query

Query multiple programs simultaneously:
const blocks = await dataSource.getBlocks({
  from: 259984950,
  to: 260000000,
  fields: {
    instruction: { programId: true, accounts: true, data: true },
  },
  instructions: [
    {
      programId: [
        "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", // Orca Whirlpool
        "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", // SPL Token
      ],
    },
  ],
});

Performance Optimization

1. Request Only Needed Fields

// Good: Minimal fields
fields: {
  instruction: {
    programId: true,
    accounts: true,
  }
}

// Avoid: Requesting unnecessary fields
fields: {
  instruction: {
    programId: true,
    accounts: true,
    data: true,        // Only if needed
    transactionIndex: true,     // Only if needed (use to match with transactions)
  }
}

2. Use Specific Filters

// Good: Filter at Portal level
instructions: [
  {
    programId: ["SpecificProgram"],
    d8: ["SpecificDiscriminator"],
  },
];

// Avoid: Fetching everything and filtering in code
instructions: []; // Gets all instructions

3. Optimal Batch Sizes

// Good: 10k-50k slots per query for most use cases
const BATCH_SIZE = 10000;

// Adjust based on:
// - More filters = larger batches OK
// - More fields = smaller batches
// - More activity (mainnet) = smaller batches

4. Parallel Queries

// Process multiple ranges in parallel
const ranges = [
  { from: 259984950, to: 260000000 },
  { from: 260000000, to: 260015000 },
  { from: 260015000, to: 260030000 },
];

const results = await Promise.all(
  ranges.map((range) =>
    dataSource.getBlocks({
      from: range.from,
      to: range.to,
      fields: {
        /* fields */
      },
    })
  )
);

Error Handling

try {
  const blocks = await dataSource.getBlocks({
    from: 259984950,
    to: 260000000,
    fields: {
      /* fields */
    },
  });
} catch (error) {
  if (error.code === "RATE_LIMIT_EXCEEDED") {
    // Handle rate limiting (Public Portal)
    console.error("Rate limit exceeded, waiting...");
    await new Promise((resolve) => setTimeout(resolve, 10000));
  } else if (error.code === "INVALID_SLOT_RANGE") {
    // Handle invalid range
    console.error("Invalid slot range");
  } else if (error.code === "NETWORK_ERROR") {
    // Handle network issues
    console.error("Network error, retrying...");
  } else {
    // Handle other errors
    console.error("Unknown error:", error);
  }
}

Common Use Cases

Use Case 1: Instruction Monitoring

Monitor specific program instructions:
const blocks = await dataSource.getBlocks({
  from: latestProcessedSlot + 1,
  fields: {
    block: { number: true, timestamp: true },
    instruction: { programId: true, accounts: true, data: true },
  },
  instructions: [
    {
      programId: [PROGRAM_ADDRESS],
      d8: [INSTRUCTION_DISCRIMINATOR],
    },
  ],
});

Use Case 2: Transaction Tracking

Track transactions from/to specific addresses:
const blocks = await dataSource.getBlocks({
  from: startSlot,
  to: endSlot,
  fields: {
    transaction: { signatures: true, feePayer: true },
  },
  transactions: [
    {
      feePayer: [WATCHED_ADDRESS],
    },
  ],
});

Use Case 3: Analytics Pipeline

Build analytics on blockchain data:
const blocks = await dataSource.getBlocks({
  from: startSlot,
  to: endSlot,
  fields: {
    block: { number: true, timestamp: true },
    transaction: { fee: true },
  },
});

// Aggregate statistics
const stats = blocks.map((block) => ({
  slot: block.header.number,
  txCount: block.transactions.length,
  totalFees: block.transactions.reduce(
    (sum, tx) => sum + BigInt(tx.fee || 0),
    0n
  ),
}));

Request Structure Reference

Every Portal request follows this structure:
{
  type: "solana",              // Chain type
  fromBlock: number,            // Starting slot number (inclusive, field name: fromBlock)
  toBlock?: number,             // Ending slot number (inclusive, optional, field name: toBlock)
  fields: {                    // What data to return
    block?: { ... },
    transaction?: { ... },
    instruction?: { ... },
    log?: { ... },
    tokenBalance?: { ... },
    balance?: { ... },
    reward?: { ... }
  },
  instructions?: [...],        // Filter by instructions
  transactions?: [...],       // Filter by transactions
  logs?: [...],               // Filter by logs
  tokenBalances?: [...],      // Filter by token balances
  balances?: [...],           // Filter by balances
  rewards?: [...]             // Filter by rewards
}

Response Format

Portal returns newline-delimited JSON (NDJSON), with one block per line:
{"header": {"number": 259984950, ...}, "instructions": [...], "transactions": [...]}
{"header": {"number": 259984951, ...}, "instructions": [...], "transactions": [...]}
{"header": {"number": 259984952, ...}, "instructions": [...], "transactions": [...]}
This format enables constant-memory streaming of arbitrary ranges.

Common Patterns

Process large slot ranges by splitting into manageable chunks:
const BATCH_SIZE = 10000;
for (let from = startSlot; from < endSlot; from += BATCH_SIZE) {
  const to = Math.min(from + BATCH_SIZE, endSlot);
  const blocks = await fetch(/* ... */);
}
Stream new blocks as they arrive using /finalized-stream:
const response = await fetch(
  'https://portal.sqd.dev/datasets/solana-mainnet/finalized-stream',
  { method: 'POST', body: JSON.stringify({
    type: 'solana',
    fromBlock: latestProcessedSlot + 1,
    fields: { /* ... */ }
  })}
);
Handle rate limits and network errors gracefully:
try {
  const response = await fetch(/* ... */);
  if (!response.ok) {
    if (response.status === 429) {
      // Rate limited - wait and retry
      await sleep(10000);
    }
  }
} catch (error) {
  // Network error - implement retry logic
}

Complete API Specification

For the complete OpenAPI specification with all available fields, options, and detailed schemas:

Full Solana API Specification

View complete OpenAPI documentation with all endpoints, fields, and options →

Next Steps

Last modified on December 8, 2025