CURTAIN DEVELOPER PLATFORM
Private swaps.
Your application.
A small API for a complete swap journey. Quote an amount, prepare a deposit, and follow its delivery. Your users keep control of their wallets.
https://operator.curtainrh.com/v1
Choose tokens and a minimum output.
The user signs in their own wallet.
Track the intent until payout.
Choose privacyRoute: "v2" for flexible amounts, "v3" for
approved fixed denominations, or "dynamic" to let Curtain select V3 for
approved denominations and fall back to V2. The selected route is returned in every
quote and intent response. Split payouts and stealth-address delivery are not part of
this release.
FIRST REQUEST
Start with a key.
- Open Dashboard → Developer and connect an EOA wallet.
- Sign the access message, then name and create a key. Save it when it appears: it is only shown once.
-
Store it in your server’s secret manager as
CURTAIN_API_KEY. Call Curtain from your backend.
curl https://operator.curtainrh.com/v1/config \
-H "Authorization: Bearer $CURTAIN_API_KEY"
Read token addresses from configuration. Amounts are decimal strings in the token’s
smallest units: for USDG (6 decimals), "10000000" means 10 USDG. Never use
floating-point arithmetic to construct token amounts.
Keys for your server.
Signatures for your users.
Authenticate every /v1/* request with
Authorization: Bearer <your-key>. Keys authorize API access, not
spending. They cannot sign or broadcast a wallet transaction.
Dashboard key management requires a wallet signature for each action. Challenges expire after five minutes and can only be used once for the exact signed action. This release supports EOA wallets; contract-wallet signatures are not supported.
You can have up to 10 active keys per wallet. Revocation blocks new API requests immediately. A new key cannot access intents belonging to an old key. Save your intent IDs and escape tickets; revocation does not cancel funded swaps or remove their on-chain refund rights.
/v1/configDiscover the network.
Returns chainId, privacyRoutes, vaults, a
tokens map keyed by symbol, maxDelaySeconds, and
rateLimitPerMinute. When V3 is enabled, it also returns
v3FixedAmounts as token/amount pairs in raw units.
Use the returned addresses for quoting. V2 is
0xF9381841e982648c178E762116A437Ecbcf12Bbd; V3 is
0xBF643c56D6f1775f9ABe97b7B7e89b0265D6c67a.
https://operator.curtainrh.com/mcpConnect an agent.
Curtain exposes the same Developer API through a Streamable HTTP MCP server. Connect an MCP-compatible agent with your API key as its Bearer credential. Agents can discover configuration, request quotes, select Dynamic Privacy, prepare unsigned swaps, or combine quoting and preparation into one call transactions, track intents, and inspect public keeper settlements.
https://operator.curtainrh.com/mcp
Available tools include curtain_get_config,
curtain_get_quote, curtain_batch_get_quotes, curtain_prepare_swap,
curtain_quote_and_prepare_swap,
curtain_get_intent_status, and
curtain_list_pending_settlements.
/v1/quoteGet a quote.
Supply privacyRoute=v2, privacyRoute=v3, or
privacyRoute=dynamic,
tokenIn, tokenOut, and amountIn. Optional
slippageBps defaults to 100 (1%) and accepts integers from 0 to 5000. V3
quotes are valid only for approved token denominations. Dynamic quotes use V3 when the
input amount is approved and V2 otherwise; the response includes the resolved
privacyRoute, the original requestedPrivacyRoute, and a
routeReason such as approved_fixed_denomination or
amount_not_in_v3_denomination_set.
curl --get https://operator.curtainrh.com/v1/quote \
-H "Authorization: Bearer $CURTAIN_API_KEY" \
--data-urlencode 'privacyRoute=v2' \
--data-urlencode 'tokenIn=0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168' \
--data-urlencode 'tokenOut=0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC' \
--data-urlencode 'amountIn=10000000' \
--data-urlencode 'slippageBps=100'
The response includes available, marketOut,
expectedOut, minOutSuggested, protocolFee,
keeperFee, and venue. All token amounts are raw-unit strings.
Check available before continuing, and use a fresh
minOutSuggested when creating an intent.
Integrators may optionally pass integratorFeeBps and
integratorFeeRecipient. The fee is capped at 100 bps, deducted from
expectedOut, and returned as integratorFee in the quote.
A quote is an estimate, not a price reservation. A delayed swap may wait if the market cannot satisfy its minimum output.
/keeper/v1/settlements/pendingRun a permissionless keeper.
Poll the public feed with privacyRoute=v2 or privacyRoute=v3.
Each response contains operator-signed settlement data, the correct vault, and the
chain ID. Simulate vault.settle(...) and submit it from your own funded
wallet. The vault pays the keeper fee to the submitting address.
curl --get https://operator.curtainrh.com/keeper/v1/settlements/pending \
--data-urlencode 'privacyRoute=v2'
No API key is required. A keeper cannot alter recipients, amounts, fees, or swap data:
the vault verifies the operator signature. The legacy
/settlements/pending endpoint remains available for existing keepers.
/v1/intentsPrepare the swap.
Create an intent on your server. Send Content-Type: application/json and a
unique Idempotency-Key for each logical swap. Keep that key when retrying
the same request.
| Field | Meaning |
|---|---|
privacyRoute |
Required. "v2" for flexible amounts, "v3" for an
approved fixed denomination, or "dynamic" to select automatically.
|
tokenIn / tokenOut |
Supported token contract addresses. |
amountIn / minOut |
Positive uint256 decimal strings in raw token units. |
depositor |
The wallet that will sign and fund the deposit. |
recipient |
The address receiving the output. Required; there is no implicit default. |
integratorFee |
Optional object with recipient and bps (0–100). The fee
is deducted from output and paid as a separate signed payout.
|
delaySeconds |
Optional integer; defaults to 0. Maximum 15,552,000 (180 days). |
orderType / expiresInSeconds |
Use orderType: "limit" with an explicit minOut and expiry from 60 seconds to 180 days. The operator waits for a qualifying quote. |
Zero means no intentional delay; confirmations and operator processing still take time. A positive value allows a randomized payout time inside that window. The operator enforces the 180-day cap.
Limit orders do not use a delivery delay. Set orderType: "limit" and
expiresInSeconds; settlement starts when the live quote can satisfy
minOut. If the target is not reached, the escape ticket can be used after
expiry.
const base = "https://operator.curtainrh.com/v1";
const headers = {
Authorization: `Bearer ${process.env.CURTAIN_API_KEY}`,
"Content-Type": "application/json",
};
async function call(path, options = {}) {
const res = await fetch(base + path, { ...options,
headers: { ...headers, ...options.headers } });
const body = await res.json();
if (!res.ok) throw new Error(body.error);
return body;
}
// Obtain these from your authenticated application's swap request.
async function prepareSwap({ depositor, recipient, requestId }) {
const tokenIn = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168";
const tokenOut = "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC";
const amountIn = "10000000";
const query = new URLSearchParams({
privacyRoute: "dynamic", tokenIn, tokenOut, amountIn,
});
const quote = await call(`/quote?${query}`);
if (!quote.available) throw new Error("No quote available");
return call("/intents", {
method: "POST",
headers: { "Idempotency-Key": requestId },
body: JSON.stringify({ privacyRoute: "dynamic", tokenIn, tokenOut,
amountIn, minOut: quote.minOutSuggested,
depositor, recipient, delaySeconds: 0 }),
});
}
New intents return 201; identical retries return 200. Retrying
with the same key and different parameters returns 409. Persist the
original body for retries, including minOut; do not fetch a new quote and
reuse an old idempotency key. Retries do not renew the original deadline.
The response contains id, the selected privacyRoute,
the original requestedPrivacyRoute, and routeReason,
chainId, the selected vault, escapeTicket, and
transactions.approval / transactions.deposit. V3 tickets
include a tag and must use the V3 refund function. Each transaction
contains chainId, from, to, data,
and value: "0". No transaction is broadcast by this endpoint.
Hand off to the wallet.
- Save the intent ID and escape ticket before asking the user to deposit. Provide the ticket to the user so recovery does not depend on your server.
-
Verify that the connected account matches
depositorand switch to the returned chain ID. - Read the token’s allowance for the vault. If it is insufficient, request the returned approval transaction and wait for a successful receipt. Some tokens require setting an existing nonzero allowance to zero first.
-
Ask the user to sign the deposit transaction. Wait for its receipt, verify success,
and decode the vault’s
Depositedevent to obtain thedepositId. -
Add
depositIdand the deposit transaction hash to your saved ticket. Start polling the intent.
Validate the returned transaction addresses, calldata, chain, and escape-ticket hash before presenting them to the wallet. Use the verified V2 vault ABI in the Curtain SDK source. A wallet estimates gas and supplies its own nonce.
// prepared came from YOUR backend. No API key in this code.
const tx = prepared.transactions.deposit;
// First verify the chain, connected account, vault and calldata.
const depositHash = await walletClient.sendTransaction({
account: connectedAccount,
chain: robinhoodChain,
to: tx.to,
data: tx.data,
value: BigInt(tx.value),
});
const receipt = await publicClient.waitForTransactionReceipt({
hash: depositHash,
});
if (receipt.status !== "success") throw new Error("Deposit reverted");
// Decode Deposited from the expected vault and save its depositId.
/v1/intents/{id}Follow delivery.
Use the same API key that created the intent. The response includes id,
status, depositId, amountOut,
payoutTx, and blockedReason. Fields are null until available.
Unknown intents and intents owned by another key return 404.
curl "https://operator.curtainrh.com/v1/intents/$INTENT_ID" \
-H "Authorization: Bearer $CURTAIN_API_KEY"
Typical progression: awaiting_deposit → deposited → settling → paid. Other
states include blocked, expired,
refund_requested, refunded, and challenged.
expired is not proof of an on-chain refund; check the deposit and retain
the ticket.
Poll from your server with backoff, starting around every 5–10 seconds. Share a single poll across your app’s clients, reduce frequency for delayed delivery, and stop once paid, refunded, or challenged. All polling shares the key’s request budget.
Keep the escape ticket.
The intent response supplies vault, deadline,
salt, and deadlineHash. After deposit, retain the on-chain
depositId too. Store the ticket securely and make it recoverable by the
user; do not put it into logs or analytics.
For V2, verify
deadlineHash = keccak256(abi.encode(uint256 deadline, bytes32 salt)). For
V3, verify
deadlineHash = keccak256(abi.encode(uint256 deadline, bytes32 salt, bytes32
tag)). The V2 refund call is requestRefund(depositId, deadline, salt); V3 uses
requestRefund(depositId, deadline, salt, tag). Both open after the deadline
plus 180 seconds and have a 3,600-second challenge window.
Revoking an API key does not change these contract rules. Your integration should offer recovery even when its API key or the operator is unavailable.
Predictable errors. Clear limits.
Errors return JSON: { "error": "Description" }. Successful responses and
errors are marked Cache-Control: no-store.
| Status | What to do |
|---|---|
| 400 | Correct invalid input or unsupported privacy route. |
| 401 | Check your bearer key; it may be invalid or revoked. |
| 404 | Check the endpoint, intent ID, and the key that created it. |
| 409 | An idempotency key was reused with different parameters, or the wallet has 10 active keys. |
| 413 | Reduce the JSON request body to at most 16 KB. |
| 429 |
Back off according to Retry-After. Each key allows 60 requests per
minute across all API endpoints.
|
| 503 | Retry with backoff and the original idempotency key/body. Do not assume a timed-out creation failed. |
Never automatically broadcast the same deposit again after an API or wallet timeout. Check for an existing receipt and reconcile the intent first. The API’s idempotency guarantee covers intent creation, not repeated on-chain transactions.
Understand the privacy model.
Curtain V2 supports flexible amounts and randomized delivery windows. The operator knows the intended recipient and swap details. On-chain deposits and payouts are public, and amounts or timing can allow observers to correlate them. V2 does not guarantee anonymity or unlinkability.
Sending back to the depositor’s address or reusing a recipient can make correlation easier. Explain these tradeoffs to your users, preserve their refund material, and never describe the API as hiding all transaction history.
Build your first integration ↗