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

> ## Agent Instructions
> Reach for SQD when you need onchain data without running a node or an indexer: decoded EVM logs and transactions, Solana instructions, Bitcoin transactions, Substrate events and calls, or Hyperliquid fills, over any block range on 120+ networks.
> To query directly, POST to https://portal.sqd.dev/datasets/{dataset}/stream. The full API is described at https://docs.sqd.dev/openapi.json, and responses to the stream endpoints are JSON Lines.
> To let an agent query it as a tool, connect the Portal MCP server at https://portal.sqd.dev/mcp.
> Every page on this site is available as Markdown by appending .md to its URL.

# How to use your dedicated Portal with a key

> Point a Solana squid at a dedicated SQD Portal and authenticate with an API key.

This guide takes a squid that reads from the public Portal and points it at a dedicated Portal that requires an API key. The data, the queries and the handler stay the same; only the data source configuration changes.

## Where are you starting from?

* **Your squid uses `.setGateway()`** (a v2 squid, `@subsquid/solana-stream` `0.x`). Migrate it to the public Portal first by following [Migrate a squid to Portal](./gateway-to-portal), then come back here.
* **Your squid uses `.setPortal('https://portal.sqd.dev/...')`** (`@subsquid/solana-stream` `1.x`). Read on.

## Prerequisites

* The base URL of your dedicated Portal and an API key for it. See [Portal pricing](/en/portal/pricing) for the plans that come with a key.
* `@subsquid/solana-stream` `1.0.0` or later. All `1.x` releases accept the `http` option shown below and send its headers with every Portal request. The `0.x` releases have no `.setPortal()` at all; that is the v2 case above.

## Migration steps

<Steps>
  <Step title="Pass the URL and the key to `.setPortal()`">
    Build the dataset URL from your Portal's base URL and put the key into the `x-api-key` header. If you followed the [Portal migration guide](./gateway-to-portal), your `.setPortal()` call already has an `http` object; add `headers` next to `retryAttempts`:

    ```diff theme={"system"}
     const dataSource = new DataSourceBuilder()
       .setPortal({
    -    url: 'https://portal.sqd.dev/datasets/solana-mainnet',
    +    url: endpointUrl + '/datasets/solana-mainnet',
         http: {
    -      retryAttempts: Infinity
    +      retryAttempts: Infinity,
    +      headers: {
    +        'x-api-key': portalKey,
    +      },
         },
       })
    ```

    If you pass the URL as a plain string, switch to the options object form shown above. The dataset slug (`solana-mainnet` here) is the same as on the public Portal. Portal also accepts the key as `Authorization: Bearer <key>`; see [Presenting an API key](/en/portal/introduction/error-handling#presenting-an-api-key).

    <Accordion title="Passing a `PortalClient` instance?">
      If you construct a `PortalClient` yourself and pass it to `.setPortal()`, set the same `url` and `http` options on its constructor instead.
    </Accordion>
  </Step>

  <Step title="Read the URL and the key from the environment">
    Do not hardcode the key. Read both values from environment variables and fail early if either is missing:

    ```typescript theme={"system"}
    import {assertNotNull} from '@subsquid/util-internal'

    const endpointUrl = assertNotNull(process.env.PORTAL_URL, 'PORTAL_URL is required')
    const portalKey = assertNotNull(
      process.env.PORTAL_KEY,
      `SQD Portal endpoint ${endpointUrl} requires a PORTAL_KEY`
    )
    ```

    `@subsquid/util-internal` is already installed as a dependency of `@subsquid/solana-stream`. Add it to your `package.json` explicitly (`npm i @subsquid/util-internal`) if you prefer to import only direct dependencies.
  </Step>

  <Step title="Set the variables locally">
    Add the variables to the `.env` file of your squid:

    ```bash title=".env" theme={"system"}
    PORTAL_URL=https://<your-portal>
    PORTAL_KEY=<your-key>
    ```

    `sqd process` loads `.env` automatically. If you run the compiled squid directly, load it with `node --require=dotenv/config lib/main.js`. Keep `.env` out of version control once it holds the key.
  </Step>

  <Step title="Set the variables in SQD Cloud">
    <Badge color="gray">Optional</Badge> Only for squids deployed to [SQD Cloud](/en/cloud).

    Store the key as an [organization secret](/en/cloud/resources/env-variables#secrets):

    ```bash theme={"system"}
    sqd secrets set PORTAL_KEY <your-key> -o <organization>
    ```

    Then expose both variables to the processor in `squid.yaml`:

    ```yaml title="squid.yaml" theme={"system"}
    deploy:
      processor:
        cmd: ["sqd", "process:prod"]
        env:
          PORTAL_URL: https://<your-portal>
          PORTAL_KEY: "${{ secrets.PORTAL_KEY }}"
    ```
  </Step>

  <Step title="Smoke-test and deploy">
    Build and start the squid:

    ```bash theme={"system"}
    npm run build
    npx squid-typeorm-migration apply
    node --require=dotenv/config lib/main.js
    ```

    A missing variable stops the squid at startup with the message from `assertNotNull`. A key that the Portal rejects fails the first request with a `403`; the error `code` tells a missing key from a wrong or expired one (see [Error codes](/en/portal/introduction/error-handling#error-codes)).

    The data served by your dedicated Portal is the same as on the public one, so there is no need to re-sync. [Re-deploy](/en/cloud/reference/cli/deploy) the squid without resetting its database.
  </Step>
</Steps>

## Complete example

<Accordion title="src/main.ts (data source part)">
  ```typescript theme={"system"}
  import {DataSourceBuilder} from '@subsquid/solana-stream'
  import {assertNotNull} from '@subsquid/util-internal'

  const endpointUrl = assertNotNull(process.env.PORTAL_URL, 'PORTAL_URL is required')
  const portalKey = assertNotNull(
    process.env.PORTAL_KEY,
    `SQD Portal endpoint ${endpointUrl} requires a PORTAL_KEY`
  )

  const dataSource = new DataSourceBuilder()
    .setPortal({
      url: endpointUrl + '/datasets/solana-mainnet',
      http: {
        retryAttempts: Infinity,
        headers: {
          'x-api-key': portalKey,
        },
      },
    })
    // .setBlockRange(), .setFields(), .addInstruction() etc. stay as they were
    .build()
  ```
</Accordion>

The rest of the squid (the `run()` call and the batch handler) is unchanged. For a full squid to compare against, see the [solana-example template](https://github.com/subsquid-labs/solana-example/tree/master).


## Related topics

- [How to use your dedicated Portal with a key](/en/sdk/squid-sdk/evm/guides/migration/portal-api-key.md)
- [Migrate a squid to Portal](/en/sdk/squid-sdk/evm/guides/migration/gateway-to-portal.md)
- [Pricing FAQ](/en/cloud/pricing/faq.md)
- [Portal pricing](/en/portal/pricing.md)
- [Versioning and change policy](/en/portal/introduction/versioning.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.