Guides

Buy or sell crypto

Ask for a quote, then accept it within a few seconds. This guide covers amounts, pending trades, the errors to expect, when a quote can be refused, and how to retry without trading twice.

Before you start

  • A key with the trade scope. Add account:read to check your balance first. We recommend an IP allowlist for the key.
  • USDC in the account to buy with, or the asset to sell. Trades settle in USDC.
  • Trading switched on for the API: GET/v1/status shows trading_enabled.

The flow

  1. Create a quote

    POST /v1/quotes with the asset, the side and an amount returns a firm price, valid for a few seconds (5 today).

  2. Accept it

    POST /v1/trades with the quote's id and an Idempotency-Key, before expires_at. The answer is the trade.

  3. Follow a pending trade

    Usually the trade is filled at once (201). If it's still pending (202), check GET /v1/trades/{id} until it's filled or failed.

Amounts

Give exactly one of quote_amount (in USDC) and base_amount (in the asset):

You want toSend
Spend 100 USDC on BTCside: "buy", quote_amount: "100.00"
Get 0.001 BTCside: "buy", base_amount: "0.001"
Sell 0.001 BTCside: "sell", base_amount: "0.001"
Sell enough BTC for 100 USDCside: "sell", quote_amount: "100.00"

The quote fills in the other amount at your account tier's price; the fee is included in the price. Amounts are decimal strings, rounded to the asset's base_increment. Each asset's smallest and largest trade, in USDC, are in GET/v1/assets, and your daily API volume is in GET/v1/limits. GET/v1/assets has every coin you can trade, and a new coin appears there as soon as Myntkaup lists it.

Create a quote

# Signed by sign-headers.sh: see Authentication
body='{"asset":"BTC","side":"buy","quote_amount":"100.00"}'
curl https://public-api.myntkaup.is/v1/quotes \
  -H "Content-Type: application/json" \
  $(./sign-headers.sh POST /v1/quotes "$body" | sed 's/^/-H /') \
  --data "$body"
import { Myntkaup } from "@myntkaup/api";

const client = new Myntkaup(); // reads the key from the environment
const quote = await client.quotes.create({ asset: "BTC", side: "buy", quoteAmount: "100.00" });
console.log(quote.id, quote.base_amount, quote.expires_at);
from myntkaup import Myntkaup

client = Myntkaup()  # reads the key from the environment
quote = client.quotes.create(asset="BTC", side="buy", quote_amount="100.00")
print(quote["id"], quote["base_amount"], quote["expires_at"])
Response · 201
{
  "id": "qt_01JB8X2Q4M7Z3V9K5T1N6R0P2W",
  "asset": "BTC",
  "side": "buy",
  "quote_currency": "USDC",
  "base_amount": "0.00104210",
  "quote_amount": "100.00",
  "price": "95960.12",
  "isk_estimate": "13421",
  "status": "open",
  "created_at": "2026-11-03T10:15:10Z",
  "expires_at": "2026-11-03T10:15:20Z",
  "trade_id": null
}
  • A quote is valid until expires_at, a few seconds after created_at (5 today), and can be accepted once.
  • A key can hold at most 5 open quotes; a sixth gets 422 limit_exceeded.
  • The quote checks the size, whether the asset can be traded and your account up front, so most problems show here rather than at the trade: amount_below_minimum, amount_above_maximum, asset_not_tradable, limit_exceeded, trading_halted, service_unavailable.
  • GET /v1/quotes/{id} shows its status: open, accepted, expired, filled or failed, and the trade_id once accepted.

Accept the quote

# Signed by sign-headers.sh: see Authentication
body='{"quote_id":"qt_01JB8X2Q4M7Z3V9K5T1N6R0P2W"}'
idempotency_key=$(uuidgen)   # reuse it to retry safely
curl https://public-api.myntkaup.is/v1/trades \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $idempotency_key" \
  $(./sign-headers.sh POST /v1/trades "$body" | sed 's/^/-H /') \
  --data "$body"
import { Myntkaup } from "@myntkaup/api";

const client = new Myntkaup(); // reads the key from the environment
// Sends a random Idempotency-Key unless you pass idempotencyKey.
const trade = await client.trades.create({ quoteId: "qt_01JB8X2Q4M7Z3V9K5T1N6R0P2W" });
console.log(trade.status, trade.base_amount);
from myntkaup import Myntkaup

client = Myntkaup()  # reads the key from the environment
# Sends a random Idempotency-Key unless you pass idempotency_key.
trade = client.trades.create(quote_id="qt_01JB8X2Q4M7Z3V9K5T1N6R0P2W")
print(trade["status"], trade["base_amount"])
Response · 201
{
  "id": "tr_01JB8X2Q4M7Z3V9K5T1N6R0P2W",
  "quote_id": "qt_01JB8X2Q4M7Z3V9K5T1N6R0P2W",
  "asset": "BTC",
  "side": "buy",
  "quote_currency": "USDC",
  "base_amount": "0.00104210",
  "quote_amount": "100.00",
  "price": "95960.12",
  "status": "filled",
  "source": "public_api",
  "created_at": "2026-11-03T10:15:12Z",
  "filled_at": "2026-11-03T10:15:13Z",
  "failure_reason": null
}

201 Created means the trade finished: status is filled, or failed with a failure_reason. 202 Accepted means it's still pending.

What can go wrong:

CodeWhat happenedWhat to do
quote_expiredThe quote's time was up.Create a new quote and accept it sooner.
price_movedThe market moved too far from the quoted price.Create a new quote.
quote_already_usedThe quote was already accepted, perhaps by an earlier try.GET /v1/quotes/{id} gives its trade_id.
insufficient_fundsThe available balance doesn't cover it any more.Check GET /v1/balances.
limit_exceededToday's API volume or trade count would be exceeded.Wait for the next UTC day.
trading_halted (503)Trading is paused, or this asset is for a few seconds.Retry after Retry-After seconds.
service_unavailable (503)We had no current price for the asset.Retry after Retry-After seconds.
too_many_concurrent_requests (429)Another trade for the account is still running.Wait for it, then retry.

When a quote is refused

A quote holds its price only for its few seconds, and only while the market stays close to it. We check the price again when you accept, and refuse the trade if the market has since moved against the quote by more than your tolerance. The rules are the same for every customer:

  • Your tolerance depends on your fee tier: half of what your fee leaves after our own trading cost, and never more than 0.5 %. The lowest fees therefore tolerate the smallest moves.

    Your feeTolerance today
    1.2 % or more0.50 %
    1.00 %0.41 %
    0.50 %0.16 %
    0.25 %0.035 %

    A move in your favour never refuses a trade; you get the quoted price either way.

  • Only current prices. We quote and fill only on a price that's at most a couple of seconds old. If we don't have one, you get 503 service_unavailable with Retry-After.

  • Fast markets. After a fast move in an asset (1 % within a minute), quotes and trades in it pause for 30 seconds: 503 trading_halted with Retry-After. The app keeps trading.

  • How fills turn out. We follow how each key's trades compare with the market afterwards. A key that accepts mostly after the market has moved against its quotes may be reviewed, and its limits lowered.

A refused trade charges nothing. Ask for a new quote.

Pending trades

A trade still running after 20 seconds, or one whose outcome needs a manual check, is answered 202 with status: "pending". The funds stay held meanwhile. Follow it with GET/v1/trades/{id}:

  • check after 1 second, then back off (2, 4, 8 seconds…);
  • stop when status is filled or failed;
  • don't start another trade with the same funds while it's pending.
Response · 202
{
  "id": "tr_01JB8X2Q4M7Z3V9K5T1N6R0P2W",
  "quote_id": "qt_01JB8X2Q4M7Z3V9K5T1N6R0P2W",
  "asset": "BTC",
  "side": "buy",
  "quote_currency": "USDC",
  "base_amount": "0.00104210",
  "quote_amount": "100.00",
  "price": "95960.12",
  "status": "pending",
  "source": "public_api",
  "created_at": "2026-11-03T10:15:12Z",
  "filled_at": null,
  "failure_reason": null
}

Timeouts and retries

If POST /v1/trades times out, or answers 502, 503 or 504, retry with the same Idempotency-Key and the same body. If the first attempt went through, you get its trade back; if not, it runs now. Never retry with a new key unless you've confirmed with GET /v1/quotes/{id} that the quote wasn't accepted. See Idempotency.

The buy() and sell() helpers

The SDKs wrap the whole flow: they create the quote, accept it, and poll a pending trade for up to 30 seconds with backoff. They return the trade.

import { Myntkaup } from "@myntkaup/api";

const client = new Myntkaup();

const bought = await client.buy({ asset: "BTC", quoteAmount: "100.00" }); // spend 100 USDC
console.log(bought.status, bought.base_amount, bought.price);

const sold = await client.sell({ asset: "BTC", baseAmount: "0.0005" }); // sell 0.0005 BTC
console.log(sold.status, sold.quote_amount);
from myntkaup import Myntkaup

client = Myntkaup()

bought = client.buy(asset="BTC", quote_amount="100.00")  # spend 100 USDC
print(bought["status"], bought["base_amount"], bought["price"])

sold = client.sell(asset="BTC", base_amount="0.0005")  # sell 0.0005 BTC
print(sold["status"], sold["quote_amount"])

A trade can still be pending after the helper's 30 seconds. Check status before you treat it as done.

Example: a recurring buy

Buys 25 USDC of BTC, trying once more with a fresh quote if the price moved. Run it from a scheduler, such as cron, once a day.

import { ApiError, Myntkaup } from "@myntkaup/api";

const client = new Myntkaup();

async function buyOnce() {
  for (let attempt = 1; attempt <= 2; attempt++) {
    try {
      return await client.buy({ asset: "BTC", quoteAmount: "25.00" });
    } catch (error) {
      const retryable = error instanceof ApiError && ["price_moved", "quote_expired"].includes(error.code);
      if (!retryable || attempt === 2) throw error;
    }
  }
}

const trade = await buyOnce();
console.log(new Date().toISOString(), trade?.id, trade?.status, trade?.base_amount);
from datetime import datetime, timezone

import myntkaup
from myntkaup import Myntkaup

client = Myntkaup()


def buy_once():
    for attempt in (1, 2):
        try:
            return client.buy(asset="BTC", quote_amount="25.00")
        except myntkaup.ApiError as error:
            if error.code not in ("price_moved", "quote_expired") or attempt == 2:
                raise


trade = buy_once()
print(datetime.now(timezone.utc).isoformat(), trade["id"], trade["status"], trade["base_amount"])

Checklist

  • The key has trade, an IP allowlist, and nothing it doesn't need.
  • Amounts are decimal strings, never floats.
  • Each trade has one Idempotency-Key, reused on every retry.
  • price_moved and quote_expired lead to a new quote, not a loop.
  • trading_halted and service_unavailable wait for Retry-After.
  • Pending trades are polled with backoff.
  • request_id is logged for every failure.