> ## 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 an EVM 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()` and `EvmBatchProcessor`** (a v2 squid). Migrate it to the public Portal first by following [Migrate a squid to Portal](./gateway-to-portal), then come back here.
* **Your squid uses `DataSourceBuilder` from `@subsquid/evm-stream` with `.setPortal('https://portal.sqd.dev/...')`.** 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.
* Any released version of `@subsquid/evm-stream` (`0.0.1` or later). All of them accept the `http` option shown below and send its headers with every Portal request. The only exception is the early `0.0.1-2-0.3887d2` prerelease, whose `.setPortal()` takes just a URL string or a `PortalClient`. If your `package.json` pins a prerelease, update to the latest version first:
  ```bash theme={"system"}
  npm i @subsquid/evm-stream@latest
  ```

## Migration steps

<Steps>
  <Step title="Pass the URL and the key to `.setPortal()`">
    Switch `.setPortal()` from the URL string form to the options object form. The dataset URL is now built from your Portal's base URL, and the key goes into the `x-api-key` header:

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

    The dataset slug (`ethereum-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="Already passing `http` options, or a `PortalClient` instance?">
      `http` replaces the HTTP client settings as a whole. If you already set other options there (e.g. `retryAttempts`), add `headers` next to them:

      ```typescript theme={"system"}
      http: {
        retryAttempts: Infinity,
        headers: { 'x-api-key': portalKey },
      },
      ```

      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/evm-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/evm-stream'
  import {assertNotNull} from '@subsquid/util-internal'
  import * as usdcAbi from './abi/usdc'

  const USDC_CONTRACT_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'

  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/ethereum-mainnet',
      http: {
        headers: {
          'x-api-key': portalKey,
        },
      },
    })
    .addLog({
      where: {
        address: [USDC_CONTRACT_ADDRESS],
        topic0: [usdcAbi.events.Transfer.topic],
      },
      range: {from: 6_082_465},
    })
    .setFields({
      log: {
        address: true,
        topics: true,
        data: true,
        transactionHash: true,
      },
    })
    .build()
  ```
</Accordion>

The rest of the squid (the `run()` call and the batch handler) is unchanged. For the full file, see the [squid-evm-rt-template](https://github.com/subsquid-labs/squid-evm-rt-template).


## Related topics

- [How to use your dedicated Portal with a key](/en/sdk/squid-sdk/solana/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.