Overview
Every endpoint is public JSON. They answer a caller that has no page open and no wallet paired — a marketplace, a bot, or another service — so nothing is authorized on a session address and nothing requires a key.
| Endpoint | Limit |
|---|---|
| POST /api/offers | 30 / min |
| POST /api/options/price | 60 / min |
| GET /api/offers/{offerId} | 60 / min |
Every amount is a decimal string with the precision it is written at,
never a float: "0.01" XCH at
precision 12, "1000" mojos of
wUSDC.b at precision 3. Ids are 64-character hex. Unix times
are seconds, and null is always
an answer — a missing expiry, a missing sample — rather than an error.
/api/offers
30 requests / minute
Import an offer
Decodes a maker's offer1… string,
substantiates its option on chain when this ledger has never seen it, and
records both so the site can advertise the offer. The offer string is the
source of truth: the price, the expiry and the option's size and strike are
read out of the spends, never from the request.
Re-importing the same string updates one row rather than adding a second,
because an offer is identified by its own content — hence the same
offerId comes back. An imported
option has no owner, so it appears on the market chain but in nobody's
positions.
The desk's Upload offer button posts here too — same throttle, same answer, and the id it returns is the page the browser is sent to.
{
"offer": "offer1qqz86…"
}
| Field | Type | Notes |
|---|---|---|
| offer | string | Required. The complete offer string, 8–16384 characters. |
curl -sS -X POST https://xchoptions.com/api/offers \
-H 'Content-Type: application/json' \
-d '{"offer":"offer1qqz86…"}'
{
"offer": {
"offerId": "3mF7pQ2xLkWs…",
"launcherId": "9f2c8d…",
"contractPuzzleHash": "b7d1e3…",
"requestedAssetId": "dddd…dd",
"requestedAmount": "1000",
"requestedPrecision": 3,
"requestedTicker": "wUSDC.b",
"fee": 0,
"expiresAtUnix": 1762145948,
"expiresOnChain": true
},
"option": {
"optionId": "9f2c8d…",
"side": "call",
"size": "0.01",
"sizeTicker": "XCH",
"strike": "1",
"strikeTicker": "wUSDC.b",
"expiresAtUnix": 1762145948,
"created": true
}
}
| Response field | Notes |
|---|---|
| offer.offerId | base58 of the sha256 of the offer string — the row's identity, not the offer's on-chain id. |
| offer.launcherId | The option's launcher id in hex. The price endpoint takes its option1… address; this value is accepted there too. |
| offer.requestedAmount | What the maker asks for, in mojos of requestedAssetId at requestedPrecision. |
| offer.fee | Mojos the offer carries for the taker's transaction, as an integer. |
| offer.expiresAtUnix | null when the offer asserts no time limit. |
| offer.expiresOnChain | false when the offer carries no time assert at all — it stays takeable until its coin is spent. |
| option.optionId | The launcher id in hex — the same value as offer.launcherId. |
| option.side | call when the contract holds XCH, put when it holds the strike asset. |
| option.size | In sizeTicker — the underlying the contract locks, total for the contract. |
| option.strike | Total strike for the whole contract, not per unit. Divide by size for the per-XCH price. |
| option.created | true when this import is what put the option in the ledger. |
- 422 — the string is not an offer, its time assert has already passed, or the chain could not substantiate the option it names.
- 503 — the node could not be reached. Not the caller's fault, and nothing was written.
- 429 — over 30 requests in a minute.
/api/options/price
60 requests / minute
Suggested price
What the model says one recorded option is worth. The number comes from the recorded spot and the realized volatility of the stored hourly series, and nothing is written back — it is an input to a decision, not a quote this site will honour.
{
"option_id": "option1nxh6jx…"
}
| Field | Type | Notes |
|---|---|---|
| option_id | string | Required. The option's option1… address — the id a wallet shows. A 64-character hex launcher id is the same 32 bytes and is accepted too, as is the camelCase optionId spelling. |
An id this ledger has never seen is not refused. The launcher carries the terms — size, strike, side, expiry — so the option is read from the chain, recorded, and priced in the same call; from then on it is one query. A launcher that is not an option, or that the chain has never seen, is a 404.
curl -sS -X POST https://xchoptions.com/api/options/price \
-H 'Content-Type: application/json' \
-d '{"option_id":"option1nxh6jx…"}'
{
"option": {
"optionId": "option1nxh6jx…",
"launcherId": "9f2c8d…",
"side": "call",
"size": "10",
"sizeTicker": "XCH",
"strike": "13",
"strikeTicker": "wUSDC.b",
"expiresAtUnix": 1769860800
},
"basis": {
"spot": 2.0,
"sigma": 0.6142,
"riskFreeRate": 0.047646653467353
},
"suggested": {
"primary": { "amount": "7", "ticker": "wUSDC.b" },
"secondary": { "amount": "3.5", "ticker": "XCH" }
}
}
| Response field | Notes |
|---|---|
| option.optionId | The option's option1… address, echoed back. option.launcherId is the same option in hex. |
| basis.spot | The last recorded spot per XCH, or null when none has been recorded. |
| basis.sigma | Annualized realized volatility from the hourly series, or null when that series is too short to form a sample. |
| basis.riskFreeRate | The rate the model was fed, as a continuously compounded fraction. It is the latest published 10-year Treasury par yield, refreshed hourly, so a suggestion above can be reproduced from this payload. |
| priceable | false when the contract's legs are not XCH and the deployment's quote asset — the desk has no per-XCH reading for it, so there is no suggestion even with a spot recorded. |
| suggested | The same premium in both legs, as decimal strings; both are totals for the contract. primary is always the deployment's quote asset and secondary always XCH — on a put those are the contract's underlying and strike legs respectively, not a reversed meaning, so a caller displays them in the order given. null when the contract is not priceable, or when there is no spot or no size to price from — an answer, not an error. |
The same quote has a page: /option/{optionId}
states the estimate and every input behind it, for a person rather than a bot.
A missing sigma is not fatal: with
no volatility sample the model falls back to intrinsic value, so the answer
is what the option is worth right now and no more. A contract whose legs are
not XCH and wUSDC.b is not priced at all — priceable
says so rather than the model guessing at a unit.
- 404 — no option is recorded for that id, and the chain does not show one.
- 422 —
option_idis missing, or is neither anoption1…address nor a 64-character hex launcher id. - 429 — over 60 requests in a minute.
- 503 — the Chia node could not be reached, so an unrecorded id could not be checked. Retry.
/api/offers/{offerId}
60 / min
Fetch a live offer
The taker's side. Returns the offer string a taker hands their wallet, for as long as the chain is still advertising that row — the offer is public, like the file a maker would send by hand.
curl -sS https://xchoptions.com/api/offers/3mF7pQ2xLkWs…
{
"offer": "offer1qqz86…"
}
404 once the offer is no
longer live — taken, cancelled, or past its time assert. The
offerId is the one
POST /api/offers answered with.
For a person, the same id has a readable breakdown at
/offers/{offerId} — the option, what the
offer gives and asks for, and when it stops existing. It renders offers that
are no longer live too, so a dead link can still say why it died.
Errors
A validation failure is the framework's own shape: a message for a human
and a per-field list for a parser. Everything else is a single
message.
{
"message": "That offer has already expired.",
"errors": {
"offer": ["That offer has already expired."]
}
}
| Status | Body | When |
|---|---|---|
| 422 | message + errors | A field is missing or unusable, the offer has expired, or the chain cannot substantiate it. |
| 404 | message | No option or live offer for that id. |
| 429 | message | Rate limit exceeded; Retry-After states the wait in seconds. |
| 503 | message | The Chia node could not be reached. Retry; nothing was recorded. |
Rate limits
Limits are per client address, counted in a rolling minute. Every answer
carries X-RateLimit-Limit and
X-RateLimit-Remaining, so a caller
can back off before it is refused rather than after.
| Endpoint | Requests / minute |
|---|---|
| POST /api/offers | 30 |
| POST /api/options/price | 60 |
| GET /api/offers/{offerId} | 60 |
The chain reads the page's own wallet scan makes — /chain/{endpoint} —
are an internal proxy for this browser, not part of the public API, and are
not supported for third-party use.