Skip to main content

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:

HeaderValue
X-ROCKY-KEY-IDIdentifier of the Rocky signing key
X-ROCKY-TIMESTAMPUnix milliseconds when the request was signed
X-ROCKY-SIGNATUREBase64 Ed25519 signature over the canonical string below
Idempotency-KeyrfqId 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"
}
FieldMeaning
sideFrom 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.
baseQtyBase-asset quantity, already rounded to the symbol's stepSize. v1 quotes are always by base quantity.
baseInstrument / quoteInstrumentToken Standard instrument ids as they will appear in the allocations.
quoteDeadlineWhen 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" }
reasonMeaning
QUOTE_EXPIREDvalidUntil passed before the call arrived
PRICE_MOVEDYour last-look check failed
INSUFFICIENT_INVENTORYYou no longer hold the output amount
RATE_LIMITEDYour own throttle
OTHERAnything 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 settlementRef with 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 is LP_ACCEPT_FAILED even if you answered ALLOCATED.
  • A REJECTED answer 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​

CallTimeoutRocky's interpretation of a non-200 or timeout
GET /health2 sOut of fan-out until healthy
POST /rfq/quotes800 msNo quote from you for this RFQ
POST …/accept-and-allocate5 sLP_ACCEPT_FAILED; trader allocation cancelled
POST …/result, …/cancel5 sLogged; 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.