Connection
| Environment | WebSocket URL |
|---|---|
| Testnet | wss://testnet.rfq.ws.injective.network/injective_rfq_rpc.InjectiveRfqRPC/TakerStream |
| Mainnet | Contact TrueCurrent for the current production endpoint |
<host>/<service>/<method> where the service is injective_rfq_rpc.InjectiveRfqRPC and the method for takers is TakerStream (makers use MakerStream). Messages are framed with gRPC-web length prefixes and use protobuf payloads. The injective-rfq-toolkit Python library and reference examples handle this framing for you. If you’re building from scratch, see src/rfq_test/clients/websocket.py for the implementation.
Authentication
Anybody can access the public RFQ streams. TakerStream is address-routed: you provide your Injective address when opening the stream, and the indexer routes quotes for your requests back to that address. The reference scripts ininjective-rfq-toolkit connect this way out of the box.
Submitting a request
An RFQ request declares what you want to trade. The indexer broadcasts it to every active maker, who each have a few hundred milliseconds to respond. Request fields:| Field | Type | Description |
|---|---|---|
client_id | string | A UUID you generate for request/ACK correlation. The indexer assigns the rfq_id; use the ACK’s rfq_id for quote collection and settlement. |
market_id | string | Injective derivative market ID (hex) |
direction | string | Lowercase "long" or "short" |
margin | string | Your margin in USDC, as a decimal string (e.g. "200") |
quantity | string | Contracts requested (e.g. "100") |
worst_price | string | Hard price limit. Quotes worse than this are rejected onchain. |
expiry | uint64 | Unix ms after which the request is ignored. Typically now + 5 min. |
request_address is required as TakerStream connection metadata. With injective-rfq-toolkit, set it on TakerStreamClient; do not put it in the RFQ request body.
Python:
rfq_id differ.
A convenience wrapper, rfq_test.factories.request.RequestFactory.create_indexer_request(...), produces the same dict shape if you’d rather not hand-build it. Both approaches are equivalent.
TypeScript:
Note: some older examples in the repo usedirection: 0against a local dev JSON-RPC indexer. On testnet (gRPC-web framed over WebSocket), the indexer expects the lowercase string form –"long"or"short".
Receiving quotes
After submitting, quotes stream in over the same connection as they’re produced by makers. Each quote is a separate message. You should expect anywhere from zero toN quotes (one per active maker) within the collection window.
Quote fields (as delivered by collect_quotes() – see websocket.py _quote_to_dict):
| Field | Type | Description |
|---|---|---|
rfq_id | string | Matches your request (note: stringified on the Python side) |
market_id | string | Matches your request |
maker | string | Maker’s inj1... address |
taker | string | Your address (echoed) |
taker_direction | string | Echoes your direction as "long" / "short" |
margin | string | Margin the maker is committing to cover their side |
quantity | string | Quantity the maker is willing to fill |
price | string | Maker’s quoted price |
expiry | int | Quote expiry (Unix ms). Cast with int(quote["expiry"]) defensively before passing to the contract. |
signature | string | Maker’s secp256k1 signature, delivered as hex with 0x prefix (e.g. "0xabc123...") |
status | string | Quote state. "pending" when first received. |
nonce | uint64 | null | Reserved for non-standard quote variants; normal RFQ quotes carry null |
Signature format: the indexer delivers the signature as hex (e.g."0xabc123..."). The onchain contract requires base64. You must convert before building theAcceptQuotemessage. See Accepting quotes.
Expiry format: here it arrives as a plain integer in Unix milliseconds. In the onchain message it must be wrapped as the Expiry enum variant – see Accepting quotes.
Collection window pattern
Because quotes arrive asynchronously, the standard pattern is:- Submit the request
- Collect incoming messages into a list, filtering by
rfq_idon a single-taker stream or by(taker, rfq_id)when routing across takers - After the collection window, stop collecting and pick the best quote(s)
- Proceed to settlement immediately
expiry. By the time you’ve accounted for maker response latency, Injective block time, and your own broadcast path, there is very little room.
- 500 ms is the current TrueCurrent default - long enough to receive quotes from warm makers, short enough to leave settlement headroom.
- Too short (<100ms): you’ll miss slower makers and reduce competition.
- Too long (>1s): quote expiry becomes a race you will lose.
worst_price, quantity ≥ your needs) and skip the window. This is lowest latency but gives worse execution.
How rfq_id correlation works
The wire protocol is slightly different from what most of the examples suggest. It’s worth understanding if you’re building a production taker.
On the wire, the taker’s outgoing request (RFQRequestInputType) carries a client_id (UUID), not an rfq_id. The indexer assigns the rfq_id when it receives the request, then broadcasts an inbound request (RFQRequestType) to all MMs with both client_id and the newly-minted rfq_id. MMs quote against the assigned rfq_id and taker address. When the taker calls collect_quotes(rfq_id=...), it’s matching on that indexer-assigned value within that taker stream. The Request unary RPC’s RequestResponse returns {status, client_id, rfq_id} so you can learn the assigned id synchronously — see the injective_rfq_rpc.proto message definitions in injective-rfq-toolkit/src/rfq_test/proto/.
Use ACK-based correlation. Generating a local millisecond timestamp and treating it as the settlement rfq_id is not reliable: the indexer assigns the actual rfq_id, and makers quote against that assigned value.
Filtering and routing
If you operate multiple takers or submit multiple concurrent RFQs, you need to route incoming quotes to the right request. Use taker address plusrfq_id as the map key; rfq_id alone is timestamp-like and can collide across takers.
Python:
Reconnection
WebSocket connections drop. Your taker must reconnect gracefully or it becomes blind. Exponential backoff:rfq_id for that taker. (Reusing the same (taker, rfq_id) pair triggers nonce replay protection onchain.)
For production systems, maintain a warm standby connection and fail over at the first dropped frame rather than tearing down and rebuilding.
Inspecting the stream
A useful debug pattern while developing is to dump every incoming message to stdout:- Check that
request_addressexactly matches the wallet you’re using - Verify there are active whitelisted makers on testnet – query the contract’s
list_makersentry - Check
configs/testnet.yamlfor the current indexer URL; the path component matters (/injective_rfq_rpc.InjectiveRfqRPC/TakerStreamfor takers,/MakerStreamfor makers) - If using gRPC-web framing manually, confirm your length-prefix and compression flag bytes are correct
Next
- Accepting quotes – turning a collected quote into an onchain settlement
- Best practices – window tuning, nonce handling, reconnection strategy

