LP Service API
Status: Specification (LP Protocol v1, draft 2026-09-23). You implement these endpoints; Rocky calls them. Field names and status values are frozen for v1.
Your service is an HTTPS API at the serviceUrl you register during onboarding. Rocky calls it from fixed egress IPs listed in your Integration Pack. All bodies are JSON; all decimals are strings; all timestamps are RFC 3339 UTC with millisecond precision.
Authentication of Rocky's requests
Every request Rocky sends carries an Ed25519 signature so that you can prove it came from Rocky:
| Header | Value |
|---|---|
X-ROCKY-KEY-ID | Identifier of the Rocky signing key |
X-ROCKY-TIMESTAMP | Unix milliseconds when the request was signed |
X-ROCKY-SIGNATURE | Base64 Ed25519 signature over the canonical string below |
Idempotency-Key | rfqId for quotes, swapId for trade calls |
Canonical string (UTF-8, \n separators, no trailing newline):
<X-ROCKY-TIMESTAMP>\n<METHOD>\n<path including query>\n<lowercase hex SHA-256 of the raw body, or of the empty string>
Rocky's public keys are published at GET /v1/lp/signing-keys and their fingerprints are read back to you over a second channel during onboarding, the same procedure as your own key in Institutional Onboarding. Reject requests whose timestamp is more than 30 s from your clock, and treat a repeated Idempotency-Key as the same request: return the same answer.
TLS 1.2+ is required. You may additionally require client certificates (mTLS); Rocky supports it on request.
GET /health
Rocky polls this every 10 s and before each RFQ fan-out. A non-200 answer or a timeout (2 s) removes you from fan-out until the next healthy answer; three consecutive failures put your LP registration in DEGRADED and page Rocky operations.
{
"status": "ok",
"party": "lp-acme::1220…",
"symbols": ["CBTC-USDCB", "CETH-USDCB"],
"time": "2026-09-23T08:00:00.000Z"
}
symbols lets you narrow fan-out at run time; it can only be a subset of the symbols registered for your LP.
POST /rfq/quotes
Rocky asks for an executable price. You have the quote timeout (800 ms by default) to answer; late or non-200 answers count as no quote.
Request:
{
"rfqId": "7d2f5a4e-1b0e-4f2a-9a5b-1c2d3e4f5a6b",
"symbol": "CBTC-USDCB",
"side": "SELL",
"baseQty": "0.0125",
"baseInstrument": { "admin": "…", "id": "CBTC" },
"quoteInstrument": { "admin": "…", "id": "USDC.B" },
"requestedAt": "2026-09-23T08:00:00.120Z",
"quoteDeadline": "2026-09-23T08:00:00.920Z"
}
| Field | Meaning |
|---|---|
side | From the trader's point of view of the base asset: SELL means the trader gives base and receives quote; BUY means the trader gives quote and receives base. |
baseQty | Base-asset quantity, already rounded to the symbol's stepSize. v1 quotes are always by base quantity. |
baseInstrument / quoteInstrument | Token Standard instrument ids as they will appear in the allocations. |
quoteDeadline | When Rocky stops waiting. |
Response 200:
{
"rfqId": "7d2f5a4e-1b0e-4f2a-9a5b-1c2d3e4f5a6b",
"quoteId": "acme-q-000123",
"price": "86206.60",
"baseQty": "0.0125",
"quoteQty": "1077.5825",
"validUntil": "2026-09-23T08:00:04.000Z",
"party": "lp-acme::1220…"
}
price is quote per base, rounded to the symbol's tickSize; quoteQty = baseQty × price, rounded to the quote asset's precision. validUntil must be at most 5 s after requestedAt. The trader receives your quoteQty net of Rocky's fees; the fees are not your concern at this step. A 204 or {"quoteId": null} means you decline to quote.
Rocky selects the best price among valid answers (highest quoteQty for SELL, lowest for BUY); ties go to the earliest answer. Only the winning LP is contacted for the trade. A quote is not a commitment to trade: your commitment is accept-and-allocate.
POST /rfq/trades/{swapId}/accept-and-allocate
The trader has accepted your quote and locked the input on ledger. Rocky has already verified the trader's allocation against the frozen swap. You have the accept timeout (5 s) to lock the output.
Request:
{
"swapId": "01a0d0f2-6c3a-7b1e-8f2a-3b4c5d6e7f80",
"quoteId": "acme-q-000123",
"symbol": "CBTC-USDCB",
"side": "SELL",
"price": "86206.60",
"baseQty": "0.0125",
"quoteQty": "1077.5825",
"trader": { "party": "rockywallet-user::1220…" },
"traderAllocation": {
"contractId": "00a1…",
"updateId": "1220…",
"instrument": { "admin": "…", "id": "CBTC" },
"amount": "0.0125"
},
"legs": {
"lpSends": { "instrument": { "admin": "…", "id": "USDC.B" }, "amount": "1077.5825", "receiver": "rockywallet-user::1220…" },
"lpFee": null
},
"settlement": {
"executor": "Rocky::1220…",
"settlementRef": { "id": "01a0d0f2-6c3a-7b1e-8f2a-3b4c5d6e7f80", "cid": null },
"requestedAt": "2026-09-23T08:00:05.000Z",
"allocateBefore": "2026-09-23T08:01:05.000Z",
"settleBefore": "2026-09-23T08:02:05.000Z"
},
"disclosedContracts": [ { "templateId": "…", "contractId": "00a1…", "createdEventBlob": "…", "synchronizerId": "…" } ]
}
legs.lpSends is exactly the allocation you must create: that instrument, that amount, that receiver, with settlement copied verbatim into the allocation's SettlementInfo. legs.lpFee is null while the maker fee is 0; when non-null it is a second allocation to Rocky's fee collector. disclosedContracts lets your participant read the trader's allocation if you want to verify it yourself.
Response 200 when you accept:
{
"swapId": "01a0d0f2-6c3a-7b1e-8f2a-3b4c5d6e7f80",
"status": "ALLOCATED",
"allocations": [
{ "contractId": "00b2…", "instrument": { "admin": "…", "id": "USDC.B" }, "amount": "1077.5825", "receiver": "rockywallet-user::1220…" }
],
"updateId": "1220…",
"disclosedContracts": [ { "templateId": "…", "contractId": "00b2…", "createdEventBlob": "…", "synchronizerId": "…" } ]
}
Response 409 when you decline:
{ "swapId": "01a0d0f2-…", "status": "REJECTED", "reason": "PRICE_MOVED" }
reason | Meaning |
|---|---|
QUOTE_EXPIRED | validUntil passed before the call arrived |
PRICE_MOVED | Your last-look check failed |
INSUFFICIENT_INVENTORY | You no longer hold the output amount |
RATE_LIMITED | Your own throttle |
OTHER | Anything else; put detail in an optional message |
Rules:
- Idempotent by
swapId. A retry of the same call must return the same answer without creating a second allocation. Rocky retries once on a transport error. - Exact amounts. Rocky reads your allocation back from the ledger and compares instrument, amount, receiver, executor and
settlementRefwith the frozen swap. Any difference is treated as a rejection (LP_ACCEPT_FAILED) and your allocation is cancelled by the executor. - Deadline. If Rocky cannot see your allocation on ledger by
allocateBefore, the swap isLP_ACCEPT_FAILEDeven if you answeredALLOCATED. - A
REJECTEDanswer or a timeout costs the trader nothing but time; Rocky cancels the trader's allocation. Repeated rejections lower your share of fan-out (see Onboarding).
POST /rfq/trades/{swapId}/result
Best-effort notification after the swap reaches a terminal status. Answer 200 with an empty body. The authoritative record is GET /v1/lp/trades/{swapId}.
{
"swapId": "01a0d0f2-…",
"status": "SUCCEEDED",
"cantonUpdateId": "1220…",
"settledAt": "2026-09-23T08:00:07.410Z"
}
For LP_ACCEPT_FAILED, SETTLEMENT_FAILED and CANCELLED the body carries reason and, if your allocation exists, lpAllocationCancelled: true|false. When it is false, withdraw it yourself after settleBefore with Allocation_Withdraw.
POST /rfq/trades/{swapId}/cancel
Sent when Rocky abandons a swap before calling accept-and-allocate completed on your side (for example the trader's allocation failed verification while your call was in flight). Answer 200. If you already allocated, withdraw after settleBefore.
Errors and timeouts, summarised
| Call | Timeout | Rocky's interpretation of a non-200 or timeout |
|---|---|---|
GET /health | 2 s | Out of fan-out until healthy |
POST /rfq/quotes | 800 ms | No quote from you for this RFQ |
POST …/accept-and-allocate | 5 s | LP_ACCEPT_FAILED; trader allocation cancelled |
POST …/result, …/cancel | 5 s | Logged; no effect on the swap |
Return 429 with Retry-After when you are overloaded; Rocky backs off fan-out to you for that many seconds.