> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kairos.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Without the SDK: plain HTTP

> The wallet API over plain HTTP: the action loop, answering every kind of step, and a complete Python client.

The SDK is a thin wrapper. Everything it does is plain JSON over HTTPS, so any language with an HTTP client and an Ethereum signing library can integrate. The full schema for every endpoint is in the [API reference](https://docs.kairos.trade/api-reference.md).

## The loop

Every action (setup, order, cancel, sign-in) is the same loop:

```mermaid theme={null}
flowchart LR
  A[POST /v3/actions] --> B{status}
  B -- needs_signature --> C[wallet signs step.request] --> D[POST /v3/actions/id] --> B
  B -- working --> E[wait, then GET /v3/actions/id] --> B
  B -- done --> F[read step.result]
  B -- failed --> G[read step.errorCode and step.error]
```

1. **Start.** `POST /v3/actions` with `{ "wallet": "0x…", "action": { … } }`.
2. **Answer.** While `status` is `needs_signature`, switch the wallet to `step.chainId`, pass `step.request` to it, and `POST /v3/actions/{id}` with the answer.
3. **Wait.** While `status` is `working`, `GET /v3/actions/{id}`: every 2 seconds, or at `holdUntilMs` when it's set.
4. **Finish.** `done` carries `result`; `failed` carries `error` and usually `errorCode`.

Send these headers on every request ([details](https://docs.kairos.trade/wallets/authentication.md)):

```http theme={null}
X-Kairos-Partner-Id: <your partner id>
X-Kairos-Wallet-Api: 2
Content-Type: application/json
```

## Step by step with curl

```bash theme={null}
BASE=https://<your base url>
H=(-H "X-Kairos-Partner-Id: $PARTNER" -H "X-Kairos-Wallet-Api: 2" -H "Content-Type: application/json")

# 1. Start an order
curl -s "${H[@]}" -X POST "$BASE/v3/actions" -d '{
  "wallet": "0x3A49EF13cb6f466E8e2B07e7f8A1DA98Ed5aD8a9",
  "action": { "type": "order", "market": "hyperliquid:11492", "outcome": "Yes", "side": "buy",
              "price": "0.01", "size": "1000", "timeInForce": "GTC", "postOnly": true }
}'
```

```json theme={null}
{
  "id": "6f0d2c7e-…",
  "type": "order",
  "status": "needs_signature",
  "request": { "method": "eth_signTypedData_v4", "params": ["0x3A49…D8a9", "{\"types\":{…},\"primaryType\":\"PlaceOrder\",…}"] },
  "description": "Confirm this Hyperliquid order",
  "chainId": "0xa4b1",
  "venue": "hyperliquid",
  "expiresAtMs": 1793130772999,
  "result": null,
  "error": null
}
```

```bash theme={null}
# 2. The wallet signs step.request; post the signature back
curl -s "${H[@]}" -X POST "$BASE/v3/actions/6f0d2c7e-…" -d '{ "signature": "0x…" }'
```

```json theme={null}
{
  "id": "6f0d2c7e-…",
  "type": "order",
  "status": "done",
  "result": {
    "orderId": "9c5e9020-4c29-4905-bd34-e589a1fd0e5f",
    "status": "executing",
    "venue": "hyperliquid",
    "market": "hyperliquid:11492",
    "venueOrderId": "100114920:588752178234",
    "venueStatus": null,
    "filledSize": "0",
    "filledNotional": "0",
    "avgPrice": null,
    "final": false,
    "success": true,
    "message": null
  },
  "error": null
}
```

## Answering each kind of step

| `request.method` | What to do | Post back |
| - | - | - |
| `eth_signTypedData_v4` | `JSON.parse(params[1])` and sign it as EIP-712 typed data with `params[0]` | `{ "signature": "0x…" }` |
| `personal_sign` | Sign the bytes in `params[0]` (hex of a UTF-8 message) as an EIP-191 message | `{ "signature": "0x…" }` |
| `wallet_sendCalls` | Send the calls from the wallet, or send each of `fallbackRequests` as a transaction | `{ "result": <the wallet's answer, or ["0xhash", …]> }` |

* **Signatures are 65 bytes** from the wallet's own key. Smart-account signatures are refused.
* **Numbers in typed data are strings.** Some signers want integers for `uint` and `int` fields: convert them first (see `as_ints` below).
* **A wrong signer answers `400 signature is not from this wallet`.** The step stays open: sign again with the right key.
* **Posting the same answer twice is safe.** Once the step has moved on, a repeat returns the action as it is now and never places a second order.
* **`409 this action is busy; GET it and retry`**: another request is answering this action. `GET` it, and post your answer again only if it still asks for the same `request`.
* A post waits up to about 25 seconds (45 on some multi-venue steps) before answering `working`. The work continues on Kairos's side either way.
* An action ends within 15 minutes and stays readable 10 minutes more. After that, its id answers `404 unknown or expired action`.

Every field name is **camelCase**. Unknown fields are ignored, so a snake\_case `time_in_force` is silently dropped and the order goes in as `GTC`. Send `price` and `size` as **strings**: a JSON number answers `422`.

## A complete Python client

This script sets the wallet up, places a resting order, signs in, reads open orders, and cancels. It signs with a local key, where a real wallet would do it through `provider.request`.

The Python examples on the other Partner API pages import `api`, `run` and `WALLET` from this file: save it as `kairos_wallet.py` next to them.

```python kairos_wallet.py theme={null}
"""Kairos wallet API (/v3) without the SDK: setup, order, sign in, read, cancel.

    pip install requests eth-account
    KAIROS_URL=https://<base url> KAIROS_PARTNER_ID=<id> PRIVATE_KEY=0x... python kairos_wallet.py hyperliquid:11492
"""
import json, os, sys, time

import requests
from eth_account import Account
from eth_account.messages import encode_defunct, encode_typed_data

BASE = os.environ["KAIROS_URL"].rstrip("/")
HEADERS = {
    "X-Kairos-Partner-Id": os.environ["KAIROS_PARTNER_ID"],
    "X-Kairos-Wallet-Api": "2",
}
account = Account.from_key(os.environ["PRIVATE_KEY"])
WALLET = account.address


def api(method, path, body=None, headers=None):
    r = requests.request(method, BASE + path, json=body, headers={**HEADERS, **(headers or {})}, timeout=60)
    data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {"error": r.text}
    if r.status_code >= 400:
        raise RuntimeError(f"{method} {path}: {r.status_code} {data}")
    return data


def as_ints(types, type_name, value):
    """eth-account wants integers where the typed data carries uint/int as strings."""
    fields = {f["name"]: f["type"] for f in types.get(type_name, [])}
    out = {}
    for name, raw in value.items():
        kind = fields.get(name, "")
        base, is_list = kind.removesuffix("[]"), kind.endswith("[]")
        if base in types:
            out[name] = [as_ints(types, base, v) for v in raw] if is_list else as_ints(types, base, raw)
        elif base.startswith(("uint", "int")) and not is_list and isinstance(raw, str):
            out[name] = int(raw, 0)
        else:
            out[name] = raw
    return out


def sign(request):
    """Sign a step's EIP-1193 request with the local key. A real wallet does this via provider.request."""
    method, params = request["method"], request["params"]
    if method == "personal_sign":
        message = encode_defunct(hexstr=params[0])
    elif method == "eth_signTypedData_v4":
        typed = json.loads(params[1])  # the typed data is a JSON string
        typed["domain"] = as_ints(typed["types"], "EIP712Domain", typed["domain"])
        typed["message"] = as_ints(typed["types"], typed["primaryType"], typed["message"])
        message = encode_typed_data(full_message=typed)
    else:
        raise ValueError(f"not a signature request: {method}")
    return "0x" + account.sign_message(message).signature.hex().removeprefix("0x")


def send_calls(step):
    """wallet_sendCalls (Predict.fun approvals): send each fallback transaction, return the hashes."""
    from web3 import Web3  # pip install web3; only Predict.fun setup needs it

    w3 = Web3(Web3.HTTPProvider("https://bsc-dataseed.binance.org"))
    hashes = []
    for req in step["fallbackRequests"]:
        call = req["params"][0]
        tx = {"to": call["to"], "data": call["data"], "value": 0, "chainId": 56,
              "nonce": w3.eth.get_transaction_count(WALLET, "pending"), "gasPrice": w3.eth.gas_price}
        tx["gas"] = w3.eth.estimate_gas({**tx, "from": WALLET})
        sent = w3.eth.send_raw_transaction(account.sign_transaction(tx).raw_transaction)
        w3.eth.wait_for_transaction_receipt(sent)  # the next nonce needs this one mined
        hashes.append("0x" + sent.hex().removeprefix("0x"))
    return hashes


def run(action):
    """Start an action and answer every step until it is done."""
    step = api("POST", "/v3/actions", {"wallet": WALLET, "action": action})
    while True:
        status = step["status"]
        if status == "done":
            return step["result"]
        if status == "failed":
            raise RuntimeError(f"{step.get('errorCode') or 'FAILED'}: {step['error']}")
        if status == "needs_signature":
            print(f"  sign on {step.get('chainId') or 'any chain'}: {step['description']}")
            if step["request"]["method"] == "wallet_sendCalls":
                answer = {"result": send_calls(step)}
            else:
                answer = {"signature": sign(step["request"])}
            step = api("POST", f"/v3/actions/{step['id']}", answer)
        else:  # working: wait for the venue, or until a delayed market's hold ends
            hold = step.get("holdUntilMs")
            time.sleep(max(0.25, hold / 1000 - time.time()) if hold else 2)
            step = api("GET", f"/v3/actions/{step['id']}")


if __name__ == "__main__":
    market = sys.argv[1] if len(sys.argv) > 1 else "hyperliquid:11492"
    venue = market.split(":")[0]

    print("status", json.dumps(api("GET", f"/v3/wallets/{WALLET}")["venues"].get(venue)))
    print("setup", run({"type": "setup", "venues": [venue]}))

    order = run({
        "type": "order", "market": market, "outcome": "Yes", "side": "buy",
        "price": "0.01", "size": "1000", "timeInForce": "GTC", "postOnly": True,
    })
    print("order", order)

    token = run({"type": "login"})["token"]
    auth = {"Authorization": f"SIWE {token}"}
    open_orders = api("GET", f"/v3/wallets/{WALLET}/orders?open=true&venue={venue}", headers=auth)["orders"]
    print("open orders", [(o["orderId"], o["status"], o["price"], o["size"]) for o in open_orders])

    print("cancel", run({"type": "cancel", "orderIds": [order["orderId"]]}))
    positions = api("GET", f"/v3/wallets/{WALLET}/positions?venue={venue}", headers=auth)["positions"]
    print("positions", len(positions["positions"]), "resolved", len(positions["resolved"]))
```

Output against staging with a Hyperliquid-funded wallet:

```text theme={null}
status {"ready": true, "next": null, "balance": "23.977265", "currency": "USDC", …}
setup {'ready': True, 'venues': {'hyperliquid': {'ready': True, 'tradingWallet': '0x3a49…d8a9'}}}
  sign on 0xa4b1: Confirm this Hyperliquid order
order {'orderId': '9c5e9020-…', 'status': 'executing', 'venueOrderId': '100114920:588752178234', 'final': False, 'success': True, …}
  sign on any chain: Sign in to show your orders and positions
open orders [('9c5e9020-…', 'live', '0.01', '1000')]
  sign on 0x89: Confirm cancelling these orders
cancel {'cancelled': ['9c5e9020-…'], 'alreadyClosed': [], 'failed': [], 'venueDetail': {}}
positions 0 resolved 0
```

### Sending approvals without `wallet_sendCalls`

Predict.fun setup asks for its approvals as one `wallet_sendCalls` on BNB Chain, and the wallet pays the gas. A wallet without `wallet_sendCalls` sends each of `fallbackRequests` (plain `eth_sendTransaction`s) instead, as `send_calls` does above. Post the hashes back as `{ "result": ["0x…", …] }`. The step then stays `working` while the approvals confirm, up to about 5 minutes.

## The same loop in JavaScript

```js theme={null}
const headers = { 'X-Kairos-Partner-Id': PARTNER, 'X-Kairos-Wallet-Api': '2', 'Content-Type': 'application/json' }
const api = async (path, body) => {
  const res = await fetch(BASE + path, { method: body ? 'POST' : 'GET', headers, body: body && JSON.stringify(body) })
  const data = await res.json().catch(() => ({}))
  if (!res.ok) throw Object.assign(new Error(data.error ?? res.statusText), { status: res.status, data })
  return data
}

async function run(wallet, action) {
  let step = await api('/v3/actions', { wallet, action })
  for (;;) {
    if (step.status === 'done') return step.result
    if (step.status === 'failed') throw Object.assign(new Error(step.error), { code: step.errorCode })
    if (step.status === 'needs_signature') {
      if (step.chainId) await ethereum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: step.chainId }] })
      if (step.request.method === 'wallet_sendCalls') {
        // Approvals: send them as a batch, or one at a time if the wallet can't batch
        const result = await ethereum.request(step.request).catch(async (e) => {
          if (![4200, 4100, -32601].includes(e.code) || !step.fallbackRequests) throw e
          const hashes = []
          for (const tx of step.fallbackRequests) hashes.push(await ethereum.request(tx))
          return hashes
        })
        step = await api(`/v3/actions/${step.id}`, { result })
      } else {
        step = await api(`/v3/actions/${step.id}`, { signature: await ethereum.request(step.request) })
      }
    } else {
      const wait = step.holdUntilMs ? Math.max(250, step.holdUntilMs - Date.now()) : 2000
      await new Promise((r) => setTimeout(r, wait))
      step = await api(`/v3/actions/${step.id}`)
    }
  }
}
```

## Retrying safely

* **Give every order a `clientOrderId`.** Starting the same order again with the same id and terms returns the existing order instead of placing a second one ([rules](https://docs.kairos.trade/wallets/reference.md#clientorderid)). On Hyperliquid the id must be `0x` plus 32 lowercase hex characters.
* **A network error after you posted a signature is not a failure.** The work runs to completion on Kairos's side. `GET /v3/actions/{id}` to see how it ended.
* **`ORDER_OUTCOME_UNKNOWN`** means the order may be live. Read `/v3/wallets/{address}/orders?open=true`, or retry with the same `clientOrderId`, before placing it again.
* **A `5xx` or a timeout on `POST /v3/actions/{id}` after an order signature** may have reached the venue. `GET` the action: it ends with the real outcome. Treat a `500`, `502`, `504` or `408` without a readable action like `ORDER_OUTCOME_UNKNOWN`.

## Errors you'll see without the SDK

Most errors are JSON: `{ "error": "message" }`, sometimes with `code`. Some come from the HTTP layer as **plain text**:

| Status | Body | Cause |
| - | - | - |
| `400` | Plain text | Malformed JSON, a path id that isn't a UUID, or a bad query value (`open` must be `true` or `false`) |
| `415` | Plain text | Missing `Content-Type: application/json` |
| `422` | Plain text | A missing required field, an unknown action `type`, a non-UUID in `orderIds`, or a number where a string is expected |

Once a signature has been posted, failures come back as **HTTP 200** with `status: "failed"`, not as error statuses. See the [reference](https://docs.kairos.trade/wallets/reference.md#errors) for every code.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.