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

# Node WebSocket protocol

> How a resolver node and the relay talk: hello, RFQ, offer, decline, heartbeat, rate band and lifecycle.

Nodes connect to `wss://<relay>/ws/node`. Messages are JSON text frames with a `type` field. The Go file
`panofx-node/internal/intake/protocol.go` is the single source of truth for the wire format; the relay's
TypeScript mirrors it.

## Connect

<Steps>
  <Step title="Challenge, relay → node">
    ```json theme={null}
    { "type": "challenge", "nonce": "<64 hex>" }
    ```
  </Step>

  <Step title="Hello, node → relay">
    ```json theme={null}
    {
      "type": "hello", "address": "0x…", "name": "Lagos desk", "version": "0.10.1",
      "corridors": [ { "chainId": 84532, "baseToken": "0x…", "quoteToken": "0x…" }, { "chainId": 103, "baseToken": "<mint>", "quoteToken": "<mint>" } ],
      "nonce": "<the challenge nonce>", "signature": "0x…",
      "solanaAddress": "<pubkey>", "solanaSignature": "<base58>"
    }
    ```

    `signature` is EIP-191 over `panofx-node-hello|<nonce>|<address>|<name>`. A node that quotes a Solana
    chain adds `solanaAddress` and an ed25519 signature over `panofx-node-hello|<nonce>|<solanaAddress>|<name>`.
    The relay keeps the corridors on chains it serves and ignores the rest. On a relay that requires
    registration, an unregistered address is closed with "the relay only admits registered resolvers".
  </Step>

  <Step title="Welcome, relay → node">
    ```json theme={null}
    { "type": "welcome", "nodeId": "…", "heartbeatIntervalMs": 10000 }
    ```
  </Step>
</Steps>

A newer connection for the same address replaces the older one, which waits 60 seconds before retrying.

## RFQ, relay → node

```json theme={null}
{
  "type": "rfq", "id": "<uuid>", "chainId": 84532,
  "sellToken": "0x…", "buyToken": "0x…", "sellAmount": "25000000000000",
  "taker": "0x…", "executor": "0x64Dc…", "replyBy": 1757750401500
}
```

`sellAmount` is base units of what the taker sells. `replyBy` is unix milliseconds. On Solana `executor` is
the program and the RFQ carries the rest of the settlement terms: `feePayer`, `feeMint`, `feeBps`,
`treasury`, `gasFee` (in the fee mint), `computeUnitLimit`, `computeUnitPrice`.

## Offer, node → relay

```json theme={null}
{
  "type": "offer", "rfqId": "<uuid>",
  "order": "0x<abi.encode(LimitOrder)>", "signature": "0x<65 bytes>",
  "swapper": "0x…", "inputToken": "0x…", "inputAmount": "15508060000",
  "outputToken": "0x…", "outputAmount": "25000000000000",
  "deadline": 1757750490, "nonce": "…"
}
```

`inputToken` is what the node gives; `outputToken` is what it receives. On Solana the offer carries
`transaction` (the maker-signed canonical settlement, base64) instead of `order`, `signature` is the
maker's base58 ed25519 signature, and `deadline` is the fill's `expire_at`. An offer carries exactly one
of `order` or `transaction`.

## Decline, node → relay

```json theme={null}
{ "type": "decline", "rfqId": "<uuid>", "reason": "ticket_cap" }
```

The reasons are listed under [Why your node declined](/resolvers/operate/declines).

## Heartbeat, node → relay, every `heartbeatIntervalMs`

```json theme={null}
{
  "type": "heartbeat", "ts": 1757750400, "address": "0x…", "version": "0.10.1",
  "state": "quoting", "degraded": "", "region": "lagos", "signerType": "keyfile",
  "inventory": [ { "chainId": 84532, "token": "0x…", "balance": "…", "allowance": "…" } ],
  "chains": [ { "chainId": 84532, "state": "quoting" }, { "chainId": 103, "state": "degraded", "degraded": "rpc_unavailable" } ],
  "signature": "0x…"
}
```

`signature` is EIP-191 over `panofx-node-heartbeat|<ts>|<address>`. `state` is `quoting` while any chain
quotes, else `degraded` or `paused`. A node is offered RFQs on a chain only while that chain's state is
`quoting`. On Solana `allowance` equals `balance`. A node whose heartbeat is older than 35 seconds is stale.

## Rate band, relay → node, every 10 seconds

```json theme={null}
{
  "type": "rate_band", "chainId": 84532, "baseToken": "0x…", "quoteToken": "0x…",
  "mid": "1612.4", "low": "1598.1", "high": "1627.9", "sources": 5, "ts": 1757750400123
}
```

Base units per quote unit: for cNGN/USDC, naira per dollar. Advisory. The relay publishes the same NGN/USD
band once for each dollar token a chain's oracle lists, with that pair's tokens.

## Lifecycle, relay → node

```json theme={null}
{ "type": "fill", "rfqId": "<uuid>", "txHash": "0x…" }
```

`type` is `fill`, `lost` or `expired`. On Solana `txHash` is the transaction signature.

## Error, relay → node

```json theme={null}
{ "type": "error", "message": "…" }
```

## Registration

`panofx-node register` posts to [`POST /v1/resolvers/register`](/integrators/api/register-resolver) with
an EIP-191 signature over `panofx-register|<chainId>|<address>|<name>|<alertEmail>|<region>|<issuedAt>`.

## Limits

|                                  |                        |
| -------------------------------- | ---------------------- |
| RFQ window                       | 1,500 ms               |
| Quote lifetime                   | 20 s                   |
| Minimum offer lifetime           | 10 s                   |
| Maximum offer lifetime on Solana | 120 s                  |
| RFQs per second to one node      | 100, burst 200         |
| Heartbeat interval               | 10 s; stale after 35 s |
