Esc

    Integrate

    Client access

    How a consumer gets a client API key listed, and how the nodes authenticate and encrypt the stream for listed clients.

    Nodes serve prices only to listed clients. The only way to receive prices is the node WebSocket /v2/ws. Every stream on it is authenticated and encrypted. There is no plaintext mode and no anonymous mode.

    You build Who holds the client key
    A Rust service with the Rust SDK Your service. It has its own key.
    A web app on the web proxy The proxy operator. One proxy key serves all its browsers.

    The client API key

    A client API key is a secp256k1 key.

    • The key only signs. It never does key agreement. Each connection uses a fresh X25519 key for that.
    • The address of the key is the client's id. It names no customer.
    • The expiry time of each client is public.
    • Use the key only for this protocol. Do not sign anything else with it.
    • Keep the key on the consumer's machine. Governance needs only the address.

    Get access

    1. Make a key on the consumer's machine. The script needs a checkout of the price-oracle repository and Rust.

      bash
      scripts/client-key.sh --out client.key

      The script prints the address and the next command:

      text
      address  0x…
      key      client.key  (keep it on the client's machine)
      
      Governance registers it with:
        scripts/add-client.sh --identity <controller> --address 0x… --expires <YYYY-MM-DD>
    2. Send the address and the expiry date that you need to governance. Never send the key file.

    3. Governance lists the address. The script calls add_client on the registry canister:

      bash
      scripts/add-client.sh --identity <controller> --address 0x… --expires 2027-01-01

      --expires takes an ISO 8601 date or time, UTC unless it names an offset, or unix seconds. On mainnet the script asks for confirmation before it publishes. ENV=local targets the local network.

    4. Each node fetches the new client list at its next poll, every 30 s by default. The key can then connect.

    The key file

    scripts/client-key.sh writes the key as plaintext hex. It runs the client_key example of oracle-client.

    • The file has mode 0600, so only its owner can read it.
    • The script never overwrites a file that exists.
    • The SDK reads no file. Your code reads the file and passes the key to the builder:
    rust
    let api_key = oracle_wire::Signer::from_hex(std::fs::read_to_string("client.key")?.trim())?;
    let builder = builder.client_key(api_key);
    • oracle-proxy refuses to start when the group or others can read its key file. The error tells you to run chmod 600 on the file.

    Governance calls

    Governance is the controller set of the registry canister. Only governance can change the client list.

    Call Candid Effect
    add_client (text, nat64) Lists an address until expires_at, in unix seconds. expires_at must be in the future.
    set_client_expiry (text, nat64) Moves the expiry of a listed client, later or earlier.
    remove_client (text) Removes a client.
    bash
    icp canister call <CANISTER_ID> add_client '("0x…", <expires_at> : nat64)' -n ic

    Rules that the canister enforces:

    • An address is listed once, ever, as a node or as a client.
    • A removed client never comes back. A consumer whose key was removed needs a new key.
    • Each call publishes a new version of the client list. The registry version does not change.

    The client list

    The client list is a second document, apart from the registry. The canister signs it with the same threshold key as the registry. It serves it as the certified document /clients.json.

    json
    {
      "version": 12,
      "effective_from": 1754582400,
      "epoch_secs": 60,
      "clients": [{ "address": "0x7f3a…", "expires_at": 1757174400 }],
      "governance_signature": "0x…"
    }
    Field Meaning
    version Increases with each change. Nodes refuse a lower version.
    effective_from The issue time of this version.
    epoch_secs The length of a broadcast-key epoch. Session deadlines align to it.
    clients The address and the expiry, in unix seconds, of each listed client.
    governance_signature The signature of the canister's threshold key.

    Consumers never download the client list. Only nodes read it.

    What the transport does

    The transport has two layers.

    Layer Scope What it does
    Per session One connection The handshake, the control channel, and the delivery of broadcast keys.
    Per update One node and one epoch One broadcast key seals each signed frame once. Every subscriber receives the same bytes.

    The Rust SDK runs all of this for you. This section tells you what it does, so that you can read its logs.

    Handshake

    text
    node   → client  challenge: the node address, and 32 fresh random bytes
    client           checks that the node address is the address it dialled
    client → node    auth: the client public key, a fresh X25519 key, a nonce, a signature
    node             checks: the client is listed, the deadline is in the future, the signature
    node   → client  auth_ok: the HPKE encapsulation, the session deadline,
                     the first broadcast keys, the node signature
    client           checks that the node signature recovers to the address it dialled
    client → node    the subscription, on the control channel
    • The client signs a transcript that binds the node address, the challenge, its nonce and its fresh X25519 key.
    • The node answers through HPKE (RFC 9180): X25519 key agreement and HKDF-SHA256. HPKE gives two session keys, one for each direction.
    • The node signs the encapsulation, the deadline and the sealed keys. A party in the middle cannot replace any of them.
    • The client sends nothing else until the node signature checks out.
    • If the challenge names another address, the node probably rotated its key. The client fetches the registry once and connects again.
    • If the node refuses the client, it sends refused with a reason and closes the connection.

    Control channel

    The control channel carries the subscriptions and the transport's own messages. AES-256-GCM-SIV seals each message. Each direction has its own key, and a counter that must increase by exactly one.

    The node uses the channel to send:

    • broadcast keys;
    • a new session deadline;
    • close, with a reason.

    Broadcast keys and epochs

    • Each node has one broadcast key for each epoch. An epoch is epoch_secs long, 60 s by default, on a fixed grid.
    • The node seals each frame once with AES-256-GCM-SIV. Every subscriber gets the same bytes.
    • Each key is bound to the node address, the epoch, a generation number, and its start and end times. The client derives the frame key from the secret and the address that it dialled. A key thus never opens frames of another node or another epoch.
    • The node sends the next key 10 s before the epoch boundary. The client keeps the old key for 5 s after the boundary, for frames in flight.
    • Each frame has a random nonce. AES-GCM-SIV resists nonce reuse: a repeated nonce shows only whether two frames were equal.

    Decryption proves nothing

    Every listed client holds the broadcast key. Any of them can seal a frame that decrypts. The SDK therefore verifies each decrypted frame like any other frame: the node signature, the registry, the sequence and the clock. A frame that a key holder signed with its own key fails these checks.

    Encryption limits who can read the stream. It does not limit who can pass it on. A listed client can forward the plaintext, and the node signatures make it verifiable. That is a contract matter, not a cryptographic one.

    Expiry

    The session deadline is the expiry of the client, rounded down to the epoch grid:

    text
    deadline = floor(expires_at / epoch_secs) × epoch_secs
    • A session gets a broadcast key only if the key ends at or before the deadline. No client holds a key for an epoch that it outlives.
    • At the deadline the node stops sending frames to the session, sends close with expired, and closes the socket.
    • If governance extends the expiry, the node pushes the new deadline on the control channel. The session continues.

    Ask governance for an extension before the expiry. After the deadline, the nodes refuse the handshake, and your feeds go halted.

    When a client is removed

    Governance removes a client with remove_client, or moves its expiry earlier with set_client_expiry. Each node then does these steps when it adopts the new list:

    1. It closes the sessions of that client at once, with close and the reason revoked.
    2. It rotates its broadcast keys off the grid. The new keys replace the current key and any next key that it already sent.
    3. It sends the new keys only to the sessions that remain.
    4. It refuses each new handshake from the removed key.

    A closed socket does not erase a key that the client already holds. The off-grid rotation is what stops a removed client from reading frames that it captures elsewhere.

    What the consumer sees:

    • The SDK logs node connection failed with the reason node closed the session: Revoked.
    • The SDK connects again, with a backoff from 100 ms to 30 s. Each handshake fails with refused: unknown client 0x….
    • No frames arrive. Once the last updates are older than max_age, each feed is halted with insufficient_nodes.

    How long a node trusts its list

    Each node polls /clients.json every 30 s by default. Each certified fetch confirms the list, also when nothing changed.

    Time without a confirmed list What the node does
    3 h It refuses new sessions.
    24 h It closes every session, with the reason stale.

    A node that reads its list from a file, for a local run, is exempt from both limits.

    Close reasons and errors

    The node ends a session with close and one of these reasons:

    Reason Cause
    expired The client reached its session deadline.
    revoked Governance removed the client or moved its expiry earlier.
    stale The node's client list is too old to trust.
    lagged The client fell too far behind.
    protocol The peer broke the protocol.

    The SDK logs a failed connection at warn level. These handshake reasons help you find the cause:

    Reason in the log Cause What to do
    refused: unknown client 0x… The address is not in the node's client list. Check the address with governance. A new listing reaches a node at its next poll.
    refused: client authorisation ended at … The client expired. Ask governance for set_client_expiry.
    refused: client signature does not verify The key does not match the transcript. Check that you load the right key file.
    refused: the node's client list is stale; try another node The node has not confirmed its client list for 3 h. Nothing on your side. The other nodes carry the feed.
    challenge is from node … The endpoint now serves another node address. Nothing. The SDK fetches the registry once and connects again.