Skip to content

Developer Integration Guide

Base URL: https://krystal-api.foreseer.workers.dev
Chain: Base (8453) | Base Sepolia (84532)
Protocol version: 1

Authentication

Dev token (testnet)

Pass a base64-encoded JSON token in the Authorization header:

Authorization: Bearer <base64({ wallet: "0x...", userId: "0x...", isMaker: boolean })>

Example payload:

json
{ "wallet": "0x27c3d04EB5655a9985816eD8f7698F00CDA2CC38", "userId": "0x27c3d04EB5655a9985816eD8f7698F00CDA2CC38", "isMaker": true }

Auth-required endpoints

  • POST /rfqs — Create RFQ
  • GET /rfqs — List open RFQs for makers
  • POST /rfqs/:rfqId/quotes — Submit a quote (maker only)
  • POST /quotes/:quoteId/accept — Accept a quote
  • POST /rfqs/:rfqId/cancel — Cancel an RFQ
  • POST /trades/:tradeId/signing-payload — Get signing payload
  • POST /trades/:tradeId/prepare-7702 — Prepare EIP-7702 batch
  • POST /trades/:tradeId/submit — Submit trade
  • GET /trades — List trades

Optional-auth endpoints

  • GET /markets/:marketId/open-rfqs — Public + own RFQs when authed

No-auth endpoints

  • GET /markets — List markets
  • GET /markets/:marketId — Market detail
  • GET /markets/recent-trades — Recent trades
  • GET /rfqs/:rfqId — RFQ detail (with ?token= for private RFQs)
  • GET /health — Health check

Idempotency

All mutation endpoints require an idempotencyKey (UUID). If a request with the same key is received, the server returns the existing result without side effects.

RFQ endpoints

Create an RFQ

POST /rfqs
Content-Type: application/json
Authorization: Bearer <token>

{
  "idempotencyKey": "uuid",
  "marketId": "0x...",
  "side": "buy",
  "baseAmount": "1000000000000000000",
  "quoteAmount": "6000000000",
  "expiresInSeconds": 60,
  "riskAckVersion": "v1",
  "visibility": "public",
  "restrictedTo": "0x..."
}

Visibility rules:

visibilityrestrictedToOrderbook
publicnot sentVisible to all
privatenot sentHidden, access via ?token=
restricted0x...Visible to owner + restrictedTo

Fetch open RFQs

GET /markets/:marketId/open-rfqs
Authorization: Bearer <token> (optional)

Returns public RFQs + the caller's own RFQs + RFQs where restrictedTo matches.

Fetch a specific RFQ

GET /rfqs/:rfqId
GET /rfqs/:rfqId?token=<shareToken>

No auth required. Private RFQs require ?token=. Restricted RFQs require matching wallet.

Cancel an RFQ

POST /rfqs/:rfqId/cancel
Authorization: Bearer <token>

{}

Only the RFQ owner can cancel. Only Open or Quoted RFQs.

Quote endpoints

Submit a quote (maker)

POST /rfqs/:rfqId/quotes
Authorization: Bearer <token> (isMaker: true)
Content-Type: application/json

{
  "idempotencyKey": "uuid",
  "baseAmount": "1000000000000000000",
  "quoteAmount": "6000000000",
  "sellerFeeBps": 50,
  "sellerNonce": "1",
  "expiresInSeconds": 30,
  "intentExpiry": "1729575000",
  "makerSignature": "0x..."
}

The makerSignature is an EIP-712 signature over the TradeIntent struct.

Accept a quote

POST /quotes/:quoteId/accept
Authorization: Bearer <token>
Content-Type: application/json

{
  "idempotencyKey": "uuid"
}

Returns { quoteId, status: "accepted", tradeId }.

EIP-712 TradeIntent schema

Domain

json
{
  "name": "KrystalEscrow",
  "version": "1",
  "chainId": 8453,
  "verifyingContract": "<EscrowSettlement address>"
}

Types

json
{
  "TradeIntent": [
    { "name": "buyer", "type": "address" },
    { "name": "seller", "type": "address" },
    { "name": "baseToken", "type": "address" },
    { "name": "quoteToken", "type": "address" },
    { "name": "baseAmount", "type": "uint256" },
    { "name": "quoteAmount", "type": "uint256" },
    { "name": "sellerFeeBps", "type": "uint16" },
    { "name": "buyerFeeBps", "type": "uint16" },
    { "name": "expiry", "type": "uint256" },
    { "name": "marketEpoch", "type": "uint256" },
    { "name": "sellerNonce", "type": "uint256" }
  ]
}

Never include marketId or feeRecipient in the signed payload.

Trade hash

solidity
bytes32 tradeHash = _hashTypedDataV4(keccak256(abi.encode(
    TRADE_INTENT_TYPEHASH,
    intent.buyer,
    intent.seller,
    intent.baseToken,
    intent.quoteToken,
    intent.baseAmount,
    intent.quoteAmount,
    intent.sellerFeeBps,
    intent.buyerFeeBps,
    intent.expiry,
    intent.marketEpoch,
    intent.sellerNonce
)));

Fee calculation

Seller-only mode (MVP)

seller_fee      = max(quoteAmount * sellerFeeBps / 10_000, minimumFee)
buyer_pays      = quoteAmount
seller_receives = quoteAmount - seller_fee
platform_fee    = seller_fee

Maximum combined fee: 500 bps (hardcoded immutable cap).

Error codes

CodeHTTPMeaning
MARKET_NOT_ACTIVE422Market is pending, blocked, or disabled
RFQ_EXPIRED422RFQ has expired
QUOTE_EXPIRED422Quote has expired
QUOTE_NOT_FOUND404Quote ID unknown
NONCE_USED409Seller nonce already consumed
FEE_BELOW_MINIMUM422Fee below registry minimum
FEE_EXCEEDS_CAP422Combined fee > 500 bps
COMPLIANCE_REJECTED403Wallet failed screening
SIMULATION_FAILED422Tx simulation reverted
INSUFFICIENT_ALLOWANCE422Insufficient token allowance
INSUFFICIENT_BALANCE422Insufficient token balance
INVALID_STATUS422Action not allowed in current state
FORBIDDEN403Not authorized
INVALID_SIGNATURE422Signature doesn't match signer
WALLET_INELIGIBLE403Jurisdiction or KYC tier blocked
INTERNAL_ERROR500Unhandled server error

Testnet addresses

ContractAddress
EscrowSettlement0x2421F2A499edAb1D1dDDf6053de0Cb82E332858E
MarketRegistry0x69a0d8fb1301D7134ECB2AB5c272a46B5294E8f6
FeeVault0xc202D91822b5e1b58219dcAdC2C87eAb6A027847

RPC: https://sepolia.base.org

Krystal OTC RFQ DEX