# Signed requests (crypto exchanges)

Most APIs accept a fixed credential: an API key or an OAuth token sent with every call. Crypto exchanges don't. Each request has to be **signed** just before it is sent:

1. The client generates a timestamp or nonce.
2. It builds a message from parts of the request, such as the method, path, query, body and timestamp.
3. It signs that message with a secret key (HMAC, Ed25519, RSA or a JWT).
4. It sends the signature, and usually the timestamp and the public key, in headers, query parameters or the body.

The signature is different for every request, so it can't be stored and replayed. Swytchcode handles this with the `request_signature` auth type: the CLI signs each request on your machine just before sending it.

---

## What you and your agents see

- **Connect once** with `swy auth connect <provider>`. The CLI asks for each field the exchange needs, such as API key, secret key, and passphrase if the exchange uses one. It checks them against the exchange when it can, then stores them encrypted on this machine.
- **Call tools as usual.** Agents never see or provide a timestamp, nonce, signature or key; those fields are left out of tool schemas. A value passed for one anyway is ignored, with a warning.
- **Every attempt is signed fresh.** A retry, or a call sent after a [human approval](/policies/human-approval/) wait, gets a new timestamp.
- **Public endpoints** (server time, tickers) are sent unsigned and work before you connect.
- **Secrets stay local.** The secret never leaves your machine, and signatures and keys are redacted from dry runs, verbose output and the audit log.

---

## Connect an exchange

```bash
swy auth connect binance
```

```text
Enter your binance API key: vmPU…
Enter your binance Secret key (hidden):
Checked the binance credentials - accepted.
Connected binance (request_signature) - stored locally, encrypted. Never leaves this machine.
```

A key pair the exchange rejects is never stored.

For scripts, CI and servers, pass every field as one JSON object on stdin:

```bash
echo '{"api_key":"…","secret_key":"…"}' | swy auth connect binance --json
```

Some exchanges use a private key in PEM form, such as a Coinbase key. Paste it with literal `\n` escapes, or paste the key file's JSON (`{"name": "...", "privateKey": "..."}`) at the first prompt.

For end users of your own app, see [Multi-tenant apps](/guides/multi-tenant/#signed-requests-crypto-exchanges).

### Clock drift

Exchanges reject a timestamp more than a few seconds off. `swy doctor` reports how far this machine's clock is from each exchange that publishes its server time. Where the exchange supports it, the CLI also reads the exchange's clock and corrects for the difference.

---

## Exchange presets

Provider owners pick one of these presets in the app when they set a library's auth type to **Signed Requests (Crypto Exchanges)**. Presets are data on the Swytchcode backend, so adding an exchange needs no CLI update.

| Exchange | Preset | Status |
|---|---|---|
| CoinSwitch PRO (Spot and Futures) | `coinswitch` | Verified |
| CoinSwitch PRO HFT and B2B | `coinswitch_hft` | Verified |
| Binance Spot (HMAC key) | `binance` | Verified |
| MEXC Spot | `mexc` | Verified |
| Kraken Spot | `kraken` | Verified |
| Bitvavo | `bitvavo` | Verified |
| WazirX | `wazirx` | Checked against docs |
| Bybit (v5, HMAC key) | `bybit` | Checked against docs |
| OKX (v5) | `okx` | Checked against docs |
| KuCoin (API key version 2+) | `kucoin` | Checked against docs |
| Bitget (v2, HMAC key) | `bitget` | Checked against docs |
| Gate (APIv4) | `gate` | Checked against docs |
| Delta Exchange | `delta` | Checked against docs |
| CoinDCX | `coindcx` | Checked against docs |
| Kraken Futures | `kraken_futures` | Checked against docs |
| Coinbase Advanced Trade (CDP key) | `coinbase_advanced` | Checked against docs |
| Coinbase Exchange | `coinbase_exchange` | Checked against docs |
| Bitfinex (v2) | `bitfinex` | Checked against docs |
| Bitstamp (v2) | `bitstamp` | Checked against docs |
| Phemex | `phemex` | Checked against docs |
| HTX (Huobi) Spot | `htx` | Checked against docs |
| Deribit | `deribit` | Checked against docs |
| Upbit | `upbit` | Checked against docs |
| Crypto.com Exchange (v1) | `cryptocom` | Checked against docs |
| Gemini | `gemini` | Checked against docs |

- **Verified:** the preset reproduces the exchange's own published example signature.
- **Checked against docs:** the preset follows the exchange's documented algorithm, but the exchange publishes no example to compare against.

CoinSwitch HFT and B2B is one provider with two key pairs. B2B master endpoints (`/dma/api/v1/master/…`) are signed with the optional master key pair, and everything else with your main or subaccount key pair.

---

## For spec authors: `x-swytchcode-signing`

A spec can declare its own signing recipe, so a library uploaded from it works without picking a preset. Put the recipe on the security scheme that carries the signature, or at the spec root to cover every operation that declares security:

This is Binance's: an HMAC-SHA256 of the query string and body, sent as a `signature` query parameter, with `timestamp` and `recvWindow` added first so they are signed too.

```yaml
components:
  securitySchemes:
    BinanceSignature:
      type: apiKey
      in: header
      name: X-MBX-APIKEY
      x-swytchcode-signing:
        version: 1
        credentials:
          - {name: api_key, label: API key}
          - {name: secret_key, label: Secret key, secret: true}
        steps:
          - {id: ts, op: time, unit: ms}
          - {id: msg, op: concat, parts: ['{query}', '{body}']}
          - {id: sig, op: hmac, alg: sha256, key: secret_key, input: '{msg}', output: hex}
        inject:
          query:
            - {name: recvWindow, value: '5000'}
            - {name: timestamp, value: '{ts}'}
        set:
          headers:
            X-MBX-APIKEY: '{api_key}'
          query:
            - {name: signature, value: '{sig}'}
        verify: {method: GET, path: /api/v3/account}
        time_sync: {path: /api/v3/time, json_path: serverTime, unit: ms, on_error: [{status: 400, body_contains: '-1021'}]}
```

When the spec is converted (Wrekenfile 2.1.0), the recipe goes into the Wrekenfile's `SIGNING` block, and methods that use it get `SECURITY: request_signature`. Every header and parameter the recipe produces is left out of the method's inputs, so Binance's required `timestamp` and `signature` parameters are dropped. Operations with `security: []` stay unsigned.

You can also pin a method's name with `x-swytchcode-canonical-id` on the operation, which is useful when an exchange has Spot and Futures endpoints that would otherwise get similar names.

A recipe chosen in the app takes precedence over the spec's. If a later upload declares a different recipe, the app shows a notice instead of replacing it.

### Recipe fields

| Field | Meaning |
|---|---|
| `version` | Recipe format version (currently `1`). |
| `credentials` | What `auth connect` asks for: `name`, `label`, `secret` (hidden input), `encoding` (`utf8`, `hex`, `base64` or `pem`), and `optional`. |
| `steps` | Named values, computed in order (see below). |
| `inject` | `query` and `body_fields` added **before** the message is built, so they are signed (`timestamp`, `recvWindow`, `nonce`). `position: first` puts a field first. |
| `set` | `headers`, `query` and `body_fields` added **after** signing. `body: empty` sends no body (Gemini). |
| `sensitive` | Extra names to redact. Values built from a credential or a signature are redacted automatically. |
| `rules` | Different schemes per `host` or `path_prefix`. The first match is used, and a request no rule matches is refused, never sent unsigned. |
| `verify` | A read-only `{method, path}` that `auth connect` calls to check new credentials. |
| `time_sync` | `{path, json_path, unit}` of the exchange's server time. Add `on_error` to re-sync and retry once after a timestamp rejection, or `proactive: true` to sync before signing. |

### Templates

Values are text with `{name}` placeholders. A placeholder can name:

- a credential;
- a step `id`;
- a request input: `method`, `method_lower`, `host`, `path`, `query`, `query_decoded`, `query_sorted`, `path_query`, `path_query_decoded`, `body` or `content_type`.

`query` and `body` are the exact bytes sent. `path_query_decoded` is the path and query with URL escapes decoded and `+` read as a space.

### Steps

| `op` | Produces |
|---|---|
| `time` | The time: `unit` is `ms`, `s`, `us`, `iso8601_ms`, `iso8601_s` or `iso8601_s_nozone`. |
| `expires` | The time plus `seconds`. |
| `nonce` | `monotonic_ms` or `monotonic_us` (strictly increasing per key, across processes), `uuid4`, or `random_hex`. |
| `literal` | A constant `value`. |
| `concat` | `parts` joined by `separator`. |
| `hash` | `sha256`, `sha384` or `sha512` of `input`. `skip_empty` gives an empty result for an empty input. |
| `hmac` | An HMAC of `input` with `key` (a credential or another step). |
| `sign` | An `ed25519` or `rsa_pkcs1_sha256` signature of `input`. |
| `jwt` | A JWT (`ES256`, `EdDSA`, `HS256` or `HS512`) from `header` and `claims` templates, with an optional `lifetime`. |
| `encode` | `base64`, `base64url`, `hex` or `urlencode` of `input`. |
| `canonical_params` | `sorted_urlencoded` or `sorted_kv_concat` parameters, or `json_query` (a JSON body as `key=value&…`). |
| `trim_prefix` | `input` without `prefix`. |

`hash`, `hmac` and `sign` take an `output` of `hex` (default), `hex_upper`, `base64`, `base64url` or `raw`.

Recipes can only combine these building blocks; there is no expression language. A recipe that uses anything the CLI doesn't know is rejected with "upgrade the CLI", and is never run partially.

See also: the [Wreken specification](https://wreken.com/specification/wreken) (section 15).
