Websockets - Authenticating user and subscribing to user channel

Hi,

I’m having trouble with Advanced Trade Websockets, in particular the process for authenticating a user and subscribing to the user channel.

Currently I do the following:

  1. Get the currently logged in user ID via the /v2/user endpoint. This request works and uses the signature/timestamp process so I think that part of my code is OK.

  2. Then use the user ID from this response to send a websocket subscribe message like

{
	"type": "subscribe",
	"channel": "user",
	"api_key": <API Key>,
	"user_id": <User ID from request above>,
	"signature": <Signature>,
	"timestamp": <Unix timestamp>
}

This looks like it might work OK. I don’t get any invalid auth or channel errors, and I receive two messages like:

Message: {
  channel: 'user',
  client_id: '',
  timestamp: '2022-11-29T05:16:36.818827334Z',
  sequence_num: 0,
  events: [ { type: 'snapshot', orders: [] } ]
}
Message: {
  channel: 'subscriptions',
  client_id: '',
  timestamp: '2022-11-29T05:16:36.818843684Z',
  sequence_num: 1,
  events: [ { subscriptions: [Object] } ]
}

The problem is then that after that, every 15 seconds I get a message like:

{ type: 'error', message: 'subscribe or unsubscribe required' }

Which makes me think I might not have authenticated and subscribed to the user channel correctly. What’s the deal with this error message?

The user channel documentation (WS Channels) is confusing because the example request doesn’t match any of the other subscribe message formats, so I’m not sure if it’s accurate. In the docs it says to send a message like below, which has type of users rather than subscribe like in all the other payloads

// Request must contain user_isd 
{
    "type": "users",
    "user_id": "5844eceecf7e803e259d0365",
    "product_id": "BTC-USD",
    /* … api_key, timestamp, etc... */
}

It’s also quite strange because in the Authentication section of the Websockets overview (WebSocket Overview), it says that you can subscribe to channels with private order information, such as the users channel

  • as far as I can tell the channel is user not users
  • it’d be great if that section included an example request payload for authenticating and subscribing, rather than only including a sample response payload.

So I guess my ultimate questions are:

  1. Are there any examples of what messages need to be sent on the websocket for authenticating a user and subscribing to the user channel to get notified for order fills?
  2. Are those subscribe/unsubscribe error messages spurious – am I already doing it correctly?

Thanks very much for your help!

Matt

This is the message I use, and it works.

    const message = {
      type: 'subscribe',
      channel: 'user',
      api_key: REDACTED,
      product_ids: ['BTC-USD', 'ETH-USD'],
      user_id: '',
    };

Exactly like that. The only part to change is your key. I played around with the user_id field for a while, but not really sure what it’s for. If you just send it as empty quotes, it works. My assumption is it has to do with something we can’t see in the beta, and you just have to send the empty quotes. They’ll either change the docs later, or change the api.

Let me know if that works

1 Like

Thanks for the response! I’ll give that a try. Two quick questions:

• do you send the signature and time stamp in that message?
• Is the product id list required?

Cheers,
Matt

Yes, and yes as far as I can tell. Sending an empty array as the product id list did nothing, although I’ve only tried the ‘user’ and ‘ticker’ channels.

Here’s the bit I use that handles the subscriptions. You can see the timestampAndSign() function returns the message, signed message, timestamp, etc in the object that is then sent as JSON.

  // SIGNATURE
  // generate a signature using CryptoJS
  function sign(str, secret) {
    const hash = CryptoJS.HmacSHA256(str, secret);
    return hash.toString();
  }
  // create the signed message
  function timestampAndSign(message, channel, products = []) {
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const strToSign = `${timestamp}${channel}${products.join(',')}`;
    const sig = sign(strToSign, secret);
    return { ...message, signature: sig, timestamp: timestamp };
  }

  // SUBSCRIPTIONS
  // list of products to subscribe to
  const products = ['BTC-USD', 'ETH-USD'];

  // send a subscribe message
  function subscribeToProducts(products, channelName, ws) {
    // console.log('products: %s', products.join(','));
    const message = {
      type: 'subscribe',
      channel: channelName,
      api_key: key,
      product_ids: products,
      user_id: '',
    };
    // sign the message
    const subscribeMsg = timestampAndSign(message, channelName, products);
    ws.send(JSON.stringify(subscribeMsg));
  }
Here
function startWebsocket(key, secret) {
  // open a socket
  open();
 
  // don't start ws if no key or secret
  if (!secret?.length || !key?.length) {
    throw new Error('websocket connection to coinbase is missing mandatory environment variable(s)');
  }
 
  // SIGNATURE
  // generate a signature using CryptoJS
  function sign(str, secret) {
    const hash = CryptoJS.HmacSHA256(str, secret);
    return hash.toString();
  }
  // create the signed message
  function timestampAndSign(message, channel, products = []) {
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const strToSign = `${timestamp}${channel}${products.join(',')}`;
    const sig = sign(strToSign, secret);
    return { ...message, signature: sig, timestamp: timestamp };
  }
 
  // SUBSCRIPTIONS
  const products = ['BTC-USD', 'ETH-USD'];
 
  // send a subscribe message
  function subscribeToProducts(products, channelName, ws) {
    // console.log('products: %s', products.join(','));
    const message = {
      type: 'subscribe',
      channel: channelName,
      api_key: key,
      product_ids: products,
      user_id: '',
    };
    // sign the message
    const subscribeMsg = timestampAndSign(message, channelName, products);
    ws.send(JSON.stringify(subscribeMsg));
  }
 
  // send an unsubscribe message
  function unsubscribeToProducts(products, channelName, ws) {
    const message = {
      type: 'unsubscribe',
      channel: channelName,
      api_key: key,
      product_ids: products,
    };
    const subscribeMsg = timestampAndSign(message, channelName, products);
    ws.send(JSON.stringify(subscribeMsg));
  }
 
  function open() {
    console.log('OPENING');
 
    // the base URL of the API
    const WS_API_URL = 'wss://advanced-trade-ws.coinbase.com';
    // create the websocket. everything else inside the open() function is specific to this socket
    let ws = new WebSocket(WS_API_URL);
 
    ws.on('open', function () {
      console.log('Socket open!');
      subscribeToProducts(products, 'ticker', ws);
      subscribeToProducts(products, 'user', ws);
    });
 
    ws.on('close', function () {
      console.log('Socket was closed. Reopens in 1 sec');
      // always reopen the socket if it closes
      setTimeout(() => {
        open();
      }, 1000);
 
    });
 
    ws.on('error', (error) => {
      console.log(error, 'error on ws connection');
    });
 
    ws.on('message', function (data) {
      const parsedData = JSON.parse(data);
      // if (parsedData.channel) {
      //   console.log(parsedData.channel, '<--channel from ws', parsedData, '<-- data from message event');
      // }
      if (parsedData.events) {
        parsedData.events.forEach(event => {
          if (event.tickers) {
            // console.log(event, event.type, 'event from ws');
            handleTickers(event.tickers);
          } else if (event.type === 'snapshot') {
            handleSnapshot(event);
          } else if (event.type === 'update' && event.orders) {
            handleOrdersUpdate(event);
          } else {
            console.log(event, event.type, 'event from ws');
          }
        });
      }
    });
 
    // destroy connection if timeout
    function timer() {
      clearTimeout(this.pingTimeout);
      // Use `WebSocket#terminate()`, which immediately destroys the connection,
      // instead of `WebSocket#close()`, which waits for the close timer.
      // Delay should be equal to the interval at which your server
      // sends out pings plus a conservative assumption of the latency.
      this.pingTimeout = setTimeout(() => {
        console.log('ending socket after timeout');
        this.terminate();
      }, 10000);
    }
 
    // start timer when socket opens
    ws.on('open', timer);
    // each message will reset the timer
    // ticker channel sends many messages so it will be sufficient to reset the timer
    ws.on('message', timer);
    // clear the timer if socket closes
    ws.on('close', function clear() {
      clearTimeout(this.pingTimeout);
    });
 
  }
 
  // EVENT HANDLERS
  function handleSnapshot(snapshot) {
    console.log(snapshot, '<-- snapshot');
    // first snapshot should be a list of active orders
    // devs have said these come in groups of 25 or less?
  }
 
  async function handleOrdersUpdate(update) {
    // do something with the array of orders
    console.log(update.orders, '<-- orders update');
    // update should look like this:
    // {
    //   type: 'update',
    //   orders: [
    //     {
    //       order_id: 'xxxxxxxxxxx',
    //       client_order_id: 'xxxxxxxxxxx',
    //       cumulative_quantity: '0.00029411',
    //       leaves_quantity: '0',
    //       avg_price: '16554.17',
    //       total_fees: '0.01217186734675',
    //       status: 'FILLED'
    //     }
    //   ]
    // }
  }
 
  function handleTickers(tickers) {
    tickers.forEach(ticker => {
      // do something with each ticker
      console.log(ticker, '<-- ticker')
      // ticker should look like this
      // {
      //   type: 'ticker',
      //   product_id: 'BTC-USD',
      //   price: '17044.29',
      //   volume_24_h: '39981.56397272',
      //   low_24_h: '16407.01',
      //   high_24_h: '17148.31',
      //   low_52_w: '15460',
      //   high_52_w: '59118.84',
      //   price_percent_chg_24_h: '3.49888784916769'
      // }
    });
  }
}

is the rest of the javascript code if you want the context. It will open the socket, subscribe to whatever channels, and destroy + reopen it if the connection drops which I’ve noticed tends to happen every few hours.

1 Like

Epic! Thanks so much @jmicko that’s super helpful.

One more question: if you comment out the line subscribeToProducts(products, 'ticker', ws); in your sample, i.e. so you’re only subscribing to the user channel, does the socket close & reopen for you every ~15 seconds? Just want to check whether I’m the only one seeing that behaviour.

I’m thinking maybe the websocket closes if you only subscribe to user or possibly the websocket closes if it doesn’t receive new messages within a 15s period of being opened.

Thanks again!

2 Likes

Yeah, that’s intentional. It’s this bit here:

    // destroy connection if timeout
    function timer() {
      clearTimeout(this.pingTimeout);
      // Use `WebSocket#terminate()`, which immediately destroys the connection,
      // instead of `WebSocket#close()`, which waits for the close timer.
      // Delay should be equal to the interval at which your server
      // sends out pings plus a conservative assumption of the latency.
      this.pingTimeout = setTimeout(() => {
        console.log('ending socket after timeout');
        this.terminate();
      }, 10000); // <-- YOU CAN CHANGE THE TIMEOUT HERE
    }

The reason being that the user channel won’t necessarily send out messages at regular intervals, but the ticker channel does. When you open the socket, it sets a 10 second timer. That timer function will reset every time the socket receives any kind of message. When I wrote the code, I needed frequent messages to keep resetting that timer. I used the ticker channel because it was the only one working at the time. Without it, you get a few messages on ‘open’, then after about 15s it will close and reopen.

Couple things you can do.

  • You could delete the whole timer thing, but sometimes the connection ends up just hanging without a ‘close’ or ‘error’ event, so if you want to keep the connection open for hours/days at a time, leave the timer function in there.
  • You could change the timer to be much longer if you expect frequent enough updates on the user channel.
  • You could keep the ticker channel, ignore the tickers, and delete the tickerHandler function.
  • Probably the best option, would be to subscribe to the ‘ticker_batch’ channel. It sends updates every five seconds, so you aren’t wasting all that connection on thousands of tickers, but you are still resetting the timer. Last time I checked however, that channel wasn’t working.

You’re welcome!

1 Like

Ah, perfect – thanks for clarifying that! I was wondering what could have been causing the websocket to close in your implementation vs. mine was staying open and had missed that part of the sample. Cheers!

1 Like