Appearance
RFQ on testnet with curl
RFQ is the instant path: you ask, makers answer within ~750 ms, and you get a firm signed quote plus ready calldata bound to your wallet. You broadcast it yourself. It fills whole or not at all. There is no partial fill and no resting order.
Base URL: https://api.textilecredit.com. Auth: a test key (tx_test_…). Live keys cannot touch chain 97.
Tokens and chain
| Chain | 97 (BNB Smart Chain testnet) |
| cNGN | 0x8a078b182bA9649c03982c2a80CDcc81cdc99dA8 (6 decimals) |
| USDT | 0x337610d27c682E347C9cD60BD4b3b107C9d34dDd (18 decimals) |
| Reactor | 0x65B2f3847b391783e15554D5f2826FBaF318a7Df |
Fund the taker wallet with the sell token + testnet BNB for gas. You approve the reactor, not Permit2. Permit2 is the maker's side.
bash
export TEXTILE_TEST_API_KEY='tx_test_…'
export CNGN=0x8a078b182bA9649c03982c2a80CDcc81cdc99dA8
export USDT=0x337610d27c682E347C9cD60BD4b3b107C9d34dDd
export TAKER=0xYourWallet # the wallet that approves and broadcastsThe chain-97 size floor is 0.01 of the named token: 10000 for cNGN (6dp), 10000000000000000 for USDT (18dp). Below that is a 400.
0. Preview (optional)
Free indicative price off the makers' published levels. No wallet, no lock, no slot. Use it while a user types.
bash
curl -sS -X POST 'https://api.textilecredit.com/v2/rfq/preview' \
-H "Authorization: Bearer $TEXTILE_TEST_API_KEY" \
-H 'Content-Type: application/json' \
-d "{\"chainId\":97,\"sellToken\":\"$CNGN\",\"buyToken\":\"$USDT\",\"sellAmount\":\"140000000\"}"Read availableSellAmount for the published depth. A preview is not a promise. The firm price can differ, and a size that previews fine can still come back no_quote.
1. Request a firm quote
This call blocks while makers answer (750 ms reply budget). Set your client timeout well above it.
Send exactly one of sellAmount (exact-input, a gross fee-inclusive spend cap) or buyAmount (exact-output, the exact amount you receive).
bash
curl -sS -X POST 'https://api.textilecredit.com/v2/rfq/request' \
-H "Authorization: Bearer $TEXTILE_TEST_API_KEY" \
-H 'Content-Type: application/json' \
-d "{
\"chainId\": 97,
\"sellToken\": \"$CNGN\",
\"buyToken\": \"$USDT\",
\"sellAmount\": \"140000000\",
\"taker\": \"$TAKER\"
}" | tee /tmp/rfq.jsonWith a partner key that is the whole body. Without a key you also send takerProof, an EIP-712 TakerControl signature by the taker wallet. See proof of control.
Either way the taker wallet must already hold the sell token: /request reads the balance before waking makers (proof of funds). Allowance isn't checked here.
The three numbers that matter:
takerPaysis exactly what leaves your wallet (maker output + protocol fee). Never more than thesellAmountyou sent. Approve this.buyAmountis what you receive.expiresAtis your accept cutoff. Past it, do not broadcast.
bash
jq '.data.status, .data.quote.takerPays, .data.quote.buyAmount, .data.quote.expiresAt' /tmp/rfq.jsonReturned exactly once
claimToken and the signed order blobs come back on this response only. GET /v2/rfq/{id} never replays them. Store the claim token. It is what authorizes cancel, submit and status.
Other direction (0.5 USDT → cNGN): flip the tokens and send "sellAmount": "500000000000000000".
No quote
status: "no_quote" is a 200, not an error. Branch on data.status.
reason | What happened |
|---|---|
no_makers_online | No maker connected on this corridor. Back off; tell us if it persists on testnet. |
no_valid_quote | Makers were asked, none priced it in time. Retrying immediately is fine. |
no_restricted_liquidity | You restricted to wallets that couldn't quote. |
If availableSellAmount comes back, that is the real depth, so retry at that size. If retryAfterMs comes back, somebody else's live quote is holding the liquidity. Wait that long.
2. Approve and broadcast
The response hands you both transactions, already encoded. value is always "0". The fee is taken in the sell token, not native.
bash
export RFQ_ID=$(jq -r '.data.rfqId' /tmp/rfq.json)
export CLAIM=$(jq -r '.data.claimToken' /tmp/rfq.json)
SWAP_TO=$(jq -r '.data.transactions.swap.to' /tmp/rfq.json)
SWAP_DATA=$(jq -r '.data.transactions.swap.data' /tmp/rfq.json)
APPROVE_TO=$(jq -r '.data.transactions.approval.to' /tmp/rfq.json)
APPROVE_DATA=$(jq -r '.data.transactions.approval.data' /tmp/rfq.json)
# Skip the approval if the standing allowance already covers takerPays.
cast send "$APPROVE_TO" "$APPROVE_DATA" --rpc-url $BSC_TESTNET_RPC --private-key $PK
cast send "$SWAP_TO" "$SWAP_DATA" --rpc-url $BSC_TESTNET_RPC --private-key $PKAlways broadcast transactions.swap. When no single maker covers the size, Textile splits it and swap is an executeBatch over every signed order; executing the top-level encodedOrder alone would under-fill the bundle.
Re-check the clock before sending. A slow approval can push you past expiresAt. If it does, request a new quote instead of broadcasting a dead one. TTL is 60 s at most and can be shorter.
Changed your mind? Release the maker's inventory instead of letting it lapse:
bash
curl -sS -X POST "https://api.textilecredit.com/v2/rfq/$RFQ_ID/cancel" \
-H "X-Rfq-Claim: $CLAIM"3. Report and track
Tell us the hash so we can track it. Idempotent on the same hash; a different hash for an already-reported RFQ is a 409.
bash
curl -sS -X POST "https://api.textilecredit.com/v2/rfq/$RFQ_ID/submit" \
-H "X-Rfq-Claim: $CLAIM" -H 'Content-Type: application/json' \
-d '{"txHash":"0xabc…"}'
curl -sS "https://api.textilecredit.com/v2/rfq/$RFQ_ID" -H "X-Rfq-Claim: $CLAIM"Reporting is a courtesy. Settlement is reconciled from the chain either way, so a failed /submit is not a failed swap. Prefer the rfq.filled webhook over polling. Note that expired and failed are provisional for 24 h: a late-indexed fill flips the status and sends a second rfq.filled with late: true.
Gotchas
- You may hold 4 outstanding RFQs. A firm quote holds its slot until it fills or its signed order deadline passes. Cancelling and reporting do not free it. Over the cap is a
429with noRetry-After. The earliestlatestOrderDeadlineamong your live quotes is when the next slot frees. - Live quotes reserve your balance. Admission is
balance ≥ committed + required, so a wallet holding 100 USDT can't hold two 100 USDT quotes open. - Exact-input is a cap, not a spend.
sellAmountis the most you'll pay;takerPaysis the actual debit. Read it if you need the exact figure. - Need an exact figure? Use
buyAmount(exact-output). It names the amount you receive, and makers price the sell side.
Scopes
| Step | Scope |
|---|---|
| Preview | none, key optional |
| Request / cancel / submit | trades:write |
| Get by id | trades:read |
Anonymous callers use the claim token instead; it isn't scoped.