# Legacy / v1 API (/arpc/v1)

> Reference for the legacy aRPC gRPC API: one bidirectional Subscribe RPC streaming decoded Solana transactions and observed slots.



The legacy API is one bidirectional RPC. Your transaction filters go up the same stream that decoded transactions, observed slots and ping responses come back down.

**Proto:** [Download `service.proto`](/proto/arpc/legacy/service.proto) (package `arpc`, service `ARPCService`). You'll see this same shape called "v1" in some integrations.

```proto
service ARPCService {
  // Subscribe to decoded transactions
  rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeResponse) {}
}
```

Connect to `http://arpc.<region>.corvus-labs.io:20202` with insecure channel credentials; [all nine regions](/arpc/regions) behave the same way. You get in on an allowlisted egress IP by default; if your dedicated deployment is token-authenticated, attach the token as request metadata:

```text
x-token: YOUR_API_TOKEN
```

Decoded transactions and slot numbers are the whole surface here. [v2](/arpc/v2) adds raw transaction bytes, an entry stream, a dedicated slot stream with status, per-filter register/unregister, acknowledged filter validation, resolved lookup-table addresses and a `sequence` counter.

## Request messages

```proto
message SubscribeRequest {
  map<string, SubscribeRequestFilterTransactions> transactions   = 1;
  optional int32                                  ping_id        = 2;
  optional bool                                   new_seen_slots = 3;
}

message SubscribeRequestFilterTransactions {
  repeated string account_include  = 2;
  repeated string account_exclude  = 3;
  repeated string account_required = 4;
}
```

Watch the field numbers on the filter message: they start at **2**, not at 1. This is the detail hand-rolled encoders get wrong.

Each map key is a filter name you choose; the values are Base58 Solana public keys.

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

### Matching rules

Inside one named filter, all three conditions apply together:

| Field              | Match rule                                  |
| ------------------ | ------------------------------------------- |
| `account_include`  | At least one listed account must be present |
| `account_required` | Every listed account must be present        |
| `account_exclude`  | No listed account may be present            |

* No include and no required accounts → broad match, minus anything excluded.
* An entirely empty filter → **every transaction**.
* When a transaction matches several named filters, `SubscribeResponse.filters` lists every matching name.

### Updating filters

Every update carries the **complete** map you want, not a patch: each non-empty map replaces the previous one wholesale. An **empty** map won't clear anything either: it reads as "no change", and every previous filter stays active. If you want to end up with no filters at all, you need a new stream.

There is no unregister message here and no `FilterValidationResult`, so nothing ever acknowledges a filter change. An invalid Base58 key comes back as gRPC `INVALID_ARGUMENT` (3) and ends the stream, so validate keys on your side first.

Push past **50** filter-update messages per second and the stream is rejected outright with gRPC `RESOURCE_EXHAUSTED` (8), reason `subscribe_rate_limit`; see [limits](/arpc/limits#filter-update-rate).

`new_seen_slots` is independent of the map, and leaving it out keeps whatever it was set to.

## Response messages

```proto
message SubscribeResponse {
  google.protobuf.Timestamp             created_at  = 1;
  repeated string                       filters     = 2;
  optional SubscribeResponseTransaction transaction = 3;
  optional int32                        ping_id     = 4;
  optional uint64                       new_slot    = 5;
}
```

`transaction`, `ping_id` and `new_slot` are optional fields rather than a `oneof`, so you branch on whichever one is actually present in a given response.

```proto
message SubscribeResponseTransaction {
  uint64 slot                           = 1;
  uint32 num_required_signatures        = 2;
  uint32 num_readonly_signed_accounts   = 3;
  uint32 num_readonly_unsigned_accounts = 4;
  bytes  recent_blockhash               = 5;

  repeated bytes                     signatures            = 6;
  repeated bytes                     account_keys          = 7;
  repeated CompiledInstruction       instructions          = 8;
  repeated MessageAddressTableLookup address_table_lookups = 9;
  google.protobuf.BoolValue          versioned             = 10;
}

message MessageAddressTableLookup {
  bytes account_key      = 1;
  bytes writable_indexes = 2;
  bytes readonly_indexes = 3;
}

message CompiledInstruction {
  uint32 program_id_index = 1;
  bytes  accounts         = 2;
  bytes  data             = 3;
}
```

* **There is no `transaction.signature` field.** Your primary signature is `signatures[0]`.
* Signatures, `account_keys`, `recent_blockhash` and the lookup-table fields are all raw bytes, not Base58 strings.
* `address_table_lookups` carries the table and the indexes, not the resolved addresses. v2's `loaded_writable_addresses` / `loaded_readonly_addresses` carry the resolved ones.
* `versioned` is a `BoolValue` wrapper, so it can be absent as well as true or false.
* What you have is a decode of observed network data, not an execution receipt: no fee, no logs, no status.

```javascript
stream.on('data', (response) => {
  if (!response.transaction) return;

  const tx = response.transaction;
  console.log({
    slot: tx.slot,
    matchedFilters: response.filters,
    primarySignature: tx.signatures?.[0]?.toString('hex'),
  });
});
```

## Observe slots

Set `new_seen_slots: true` and the same stream delivers `new_slot` values.

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

`new_slot` is a slot aRPC has started seeing, and that is all it tells you: no status, no commitment, and no legacy equivalent of v2's `SLOT_STATUS_DEAD` / `SLOT_STATUS_COMPLETE`.

## Ping

```javascript
stream.write({ ping_id: 42 });
```

```javascript
if (response.ping_id === 42) {
  console.log('pong');
}
```

Not every response `ping_id` corresponds to a ping you sent: the server also emits an unsolicited heartbeat every **10 seconds** on this stream, a `SubscribeResponse` whose only populated field is `ping_id = 0`. Match by value when you care which pong you have. See [limits](/arpc/limits#heartbeats).

## Errors and reconnects

| Signal                                                                   | Meaning                                                                                | Action                                                                         |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `UNAUTHENTICATED` (16)                                                   | A token endpoint rejected `x-token`                                                    | Fix the credential; retrying unchanged fails identically                       |
| `PERMISSION_DENIED` (7), detail starting `PPS limit requirement not met` | The PPS condition on the streaming endpoint; the server then shuts the connection down | See [limits](/arpc/limits#the-pps-check); reconnecting alone does not clear it |
| `INVALID_ARGUMENT` (3)                                                   | Invalid public key in a filter                                                         | Fix the filter, then open a new stream                                         |
| RPC fails immediately after connect                                      | Egress IP is not allowlisted                                                           | Register the real public egress address                                        |

There is no resume and no replay, so every new stream starts from nothing: connect from an allowlisted IP, re-attach `x-token` if you use one, send the complete filter map again, restore `new_seen_slots`, recreate your ping timers, and reconcile the gap through Solana RPC if completeness matters.

<Callout type="warn" title="Pre-execution data">
  Transactions on this stream haven't executed yet. Confirm by signature through Solana RPC before you do anything irreversible.
</Callout>
