# aRPC quick start (/arpc/quickstart)

> Open a pre-execution Solana transaction stream over aRPC gRPC from Node.js, with account filters.



This gets you a working aRPC transaction stream on the legacy `Subscribe` RPC. If you end up on [v2](/arpc/v2) instead, the endpoint, port and access rules are all the same.

| You need                                                    | Where it comes from                                                                    |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Your assigned region                                        | [Dashboard](https://dashboard.corvus-labs.io); see [all nine endpoints](/arpc/regions) |
| Your public egress IP registered on the allowlist           | [Dashboard](https://dashboard.corvus-labs.io)                                          |
| An `x-token`, **only** if your dedicated deployment has one | Onboarding                                                                             |
| Node.js 20 or newer                                         | —                                                                                      |

```bash
export ARPC_ENDPOINT='http://arpc.fra.corvus-labs.io:20202'

# Only for a token-authenticated endpoint.
export ARPC_TOKEN=''
```

<Steps>
  <Step>
    ### Download the proto

    ```bash
    mkdir -p proto/arpc

    curl -fsSLo proto/arpc/service.proto \
      https://docs.corvus-labs.io/proto/arpc/legacy/service.proto
    ```

    For v2 you want both files, and the import paths have to survive the download, because `service.proto` imports `arpc/common/types.proto`:

    ```bash
    mkdir -p proto/arpc/v2 proto/arpc/common

    curl -fsSLo proto/arpc/v2/service.proto \
      https://docs.corvus-labs.io/proto/arpc/v2/service.proto

    curl -fsSLo proto/arpc/common/types.proto \
      https://docs.corvus-labs.io/proto/arpc/common/types.proto
    ```
  </Step>

  <Step>
    ### Install a gRPC client

    ```bash
    npm install @grpc/grpc-js @grpc/proto-loader
    ```

    The example loads the proto at runtime, so there's no code-generation step.
  </Step>

  <Step>
    ### Subscribe to transactions

    Create `subscribe.js`:

    ```javascript
    const grpc = require('@grpc/grpc-js');
    const protoLoader = require('@grpc/proto-loader');

    const endpoint = process.env.ARPC_ENDPOINT;
    const token = process.env.ARPC_TOKEN;

    if (!endpoint) {
      throw new Error('Set ARPC_ENDPOINT');
    }

    // grpc-js dials a bare host:port target, so take the host out of the URL.
    const { host } = new URL(endpoint);

    const definition = protoLoader.loadSync('./proto/arpc/service.proto', {
      keepCase: true,
      longs: String,
      enums: String,
      defaults: false,
      oneofs: true,
    });
    const arpc = grpc.loadPackageDefinition(definition).arpc;

    // aRPC always takes insecure channel credentials.
    const client = new arpc.ARPCService(host, grpc.credentials.createInsecure());

    const metadata = new grpc.Metadata();
    if (token) {
      metadata.set('x-token', token);
    }

    const stream = client.Subscribe(metadata);

    stream.on('data', (response) => {
      if (response.transaction) {
        const tx = response.transaction;
        console.log('transaction', {
          slot: tx.slot,
          matchedFilters: response.filters,
          // There is no singular `signature` field: the primary signature
          // is the first element of `signatures`.
          signature: tx.signatures?.[0]?.toString('hex'),
        });
      }

      if (response.new_slot !== undefined) {
        console.log('slot observed', response.new_slot);
      }

      if (response.ping_id !== undefined) {
        console.log('pong', response.ping_id);
      }
    });

    stream.on('error', (error) => {
      console.error('stream error', error.code, error.details);
    });

    stream.on('end', () => {
      console.log('stream ended');
    });

    stream.write({
      transactions: {
        'token-program': {
          account_include: [
            'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
          ],
        },
      },
      new_seen_slots: true,
    });
    ```

    Run it:

    ```bash
    node subscribe.js
    ```

    `token-program` is a name you picked, and `response.filters` echoes back every one of your filters that a transaction matched.
  </Step>
</Steps>

## Verify the stream

| Symptom                                                             | Cause                                                           | Fix                                                                        |
| ------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------- |
| No messages at all, no error                                        | The filter matches nothing                                      | Widen it, or subscribe to a busy program key like the one above            |
| Immediate `UNAUTHENTICATED` (16)                                    | Token endpoint, missing or wrong `x-token`                      | Correct the credential; do not retry the same one                          |
| RPC fails right after connecting                                    | Egress IP not on the allowlist                                  | Register the address traffic actually leaves from                          |
| `INVALID_ARGUMENT` (3), stream ends                                 | An invalid Base58 key in the filter                             | Fix the key and open a new stream; legacy has no filter-validation message |
| `PERMISSION_DENIED` (7) with detail `PPS limit requirement not met` | Plan RPS + TPS below 500; the server then closes the connection | See [limits](/arpc/limits#the-pps-check)                                   |

## Reconnect

Neither API replays anything, so a reconnect is a fresh subscription:

1. Reconnect from an allowlisted egress IP, re-attaching `x-token` only if your endpoint uses one.
2. Send the **complete** filter map again, plus `new_seen_slots`; nothing is remembered for you.
3. On v2, re-register every `filter_id` and wait for its `FilterValidationResult`; the new stream's `sequence` starts over.
4. Reconcile anything you missed through Solana RPC.

For liveness you have a real number rather than a guess: the server heartbeats every **10 seconds**, on both APIs. That's why the example above prints `pong 0` every ten seconds without you ever writing a ping: the legacy heartbeat is a `SubscribeResponse` carrying only `ping_id = 0`. Tear the stream down yourself once nothing has arrived for a comfortable multiple of that.

On v2, `sequence` is a contiguous per-stream counter rather than a loss detector; [limits](/arpc/limits#sequence-numbers) covers it, including the one entry-stream exception.

<Callout type="warn" title="Pre-execution data">
  A transaction on this stream hasn't executed yet. Confirm it by signature through Solana RPC before you treat it as done.
</Callout>

## Next

* [Legacy API reference](/arpc/v1)
* [v2 API reference](/arpc/v2)
* [Limits & operations](/arpc/limits)
