> For the complete documentation index, see [llms.txt](https://docs.strikelabs.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.strikelabs.app/api-reference/signing.md).

# Signing

Every mutating request carries a signature over a plain-text envelope. This page specifies the envelopes exactly and gives golden vectors — real signatures, from real keys — that you can verify your implementation against **before** sending a single request. Getting this right offline is much cheaper than debugging 401s.

## Why a session key

A wallet prompt per order is fine for a transfer and intolerable for trading. So the wallet signs exactly one thing — an approval naming a keypair you generated and an expiry — and that key signs every write afterwards with no prompts. The venue still holds a signature over every order it matches, from a key the account holder demonstrably authorised.

A bot holding its master key can sign directly, or approve a session key of its own. The code path is identical.

{% hint style="warning" %}
**The bound on a leaked key**

A session key acts for exactly one account, cannot approve another key, and dies at its own approval's expiry — at most 30 days out. In `deposits` mode it **can** request a withdrawal, though only ever to the account's own linked wallet address.

Note that the approval text a wallet displays still reads *"It cannot withdraw funds"*. That is accurate for paper mode and **inaccurate for `deposits` mode**, where the withdraw envelope exists precisely so a session key can. Recorded here rather than quietly contradicted; correcting the wallet-facing string is the operator's call, because a user signs that text.
{% endhint %}

## The primitive

Everything is EIP-191 `personal_sign` over UTF-8 text, recovered to an Ethereum address. Nothing here uses EIP-712.

```
digest    = keccak256("\x19Ethereum Signed Message:\n" + len(bytes) + bytes)
signature = 65 bytes: r ‖ s ‖ v      hex, "0x"-prefixed, v in {27, 28}
```

{% hint style="danger" %}
**The length prefix is the BYTE length**

Not the character count. They differ the moment the text is not ASCII, and the difference recovers a different address. The envelopes below are ASCII in practice, but implement it correctly anyway — the approval text is under your control only until an address or a symbol is not what you expected.
{% endhint %}

## The five envelopes

One human-readable approval the *wallet* signs, and four machine envelopes the *session key* signs. Fields are `key: value` lines in a fixed order, joined with `\n`, with **no trailing newline**.

**Each envelope names its operation on the first line.** That is load-bearing: it is what stops a signature taken for one kind of mutation being spent on another, whatever the remaining fields happen to say. An order signature is not a withdrawal signature.

### Three rules that hold for all of them

* **Addresses are lowercased before signing.** A checksummed address recovers correctly but hashes differently, so the venue's rebuilt envelope would not match yours.
* **Amounts are integer micros** (1e-6), never decimals. `2.5` and `2.50` are the same number and different strings, and a signature does not survive the difference. Micros are what the engine matches on anyway. Round **half away from zero** — note that Python's built-in `round()` is banker's rounding and will disagree.
* **A newline in any field is refused, not encoded.** This is the whole of the injection defence and it has to be: a `seriesId` of `"X\nside: sell"` would make a buy envelope and a sell envelope the same bytes. Fixed field order does not save you; refusing the separator does. The same applies to a comma inside a cancel symbol.

## 1. Session approval — signed by the WALLET

The only Strike prompt a wallet ever shows, so it is human-readable on purpose. Note the blank lines; they are part of the bytes.

```
Strike: approve a trading session.

This lets this device place orders on your behalf until it expires.
It cannot withdraw funds, move positions, or approve another session.

Account: {account_lowercase}
Session key: {session_key_lowercase}
Expires: {expires_at_ms}
```

POST it to `/v1/auth/session` with `address`, `sessionKey`, `expiresAt` and `signature`. No bearer token is needed — the signature *is* the authority. `expiresAt` must be in the future and within 30 days (`2592000000` ms).

## 2. Order

```
Strike order v1
account: {account_lowercase}
series: {seriesId}
side: {buy|sell}
qty: {qty_micro}
price: {price_micro|market}
nonce: {nonce_ms}
```

`price` is the literal word `market` for a market order. **`price: 0` is a real limit price at zero and a different envelope** — do not conflate "no price" with zero.

## 3. Cancel

```
Strike cancel v1
account: {account_lowercase}
symbols: {SORTED,UPPERCASE,CSV|all}
nonce: {nonce_ms}
```

Symbols are uppercased then **sorted**, joined with `,`, so the same request expressed in a different order is the same envelope and you are not forced to sort before signing. An empty scope means the whole account and is spelled `all`.

## 4. Margin

```
Strike margin v1
account: {account_lowercase}
enabled: {true|false}
nonce: {nonce_ms}
```

Turning margin off under a short is a different act from turning it on, and the signature must not cover both.

## 5. Withdraw

```
Strike withdraw v1
account: {account_lowercase}
amount: {amount_micro}
nonce: {nonce_ms}
```

The one envelope that authorises money leaving, so the amount is inside it: a signature for $10 must not be spendable on $10,000. There is **no destination field** — the venue pays only the account's own linked address, so there is nothing for a signature to authorise about it. Four lines, exactly.

## Golden vectors

These are the cross-language vectors the venue's own test suite asserts against. Reproduce them exactly and your signer agrees with the engine. They are fixed points — nothing here is judged against wall-clock time, so they do not rot.

| NAME                | VALUE                                           |
| ------------------- | ----------------------------------------------- |
| Wallet private key  | `0x0909…09` — the byte `0x09` repeated 32 times |
| Wallet address      | `0x58da990a8f4a3a6ca7cb6315d68a140105917352`    |
| Session private key | `0x1111…11` — the byte `0x11` repeated 32 times |
| Session key address | `0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a`    |
| `expiresAt`         | `1756404800000`                                 |
| `nonce`             | `1755800000123`                                 |

### Vector 1 — the approval

Exact bytes signed, with `\n` shown explicitly:

```
Strike: approve a trading session.\n
\n
This lets this device place orders on your behalf until it expires.\n
It cannot withdraw funds, move positions, or approve another session.\n
\n
Account: 0x58da990a8f4a3a6ca7cb6315d68a140105917352\n
Session key: 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a\n
Expires: 1756404800000
```

Signed with the **wallet** key:

```
0x2e02b5b36576570351b76f16f68d461f23403ef5ab087922b4f89547d30c2abc
  0a57b117c37dcf0da74b0c66ce9e6a1889f5cc99a51dcdac790476b3fdcf5a881c
```

### Vector 2 — an order

Buy 1 contract of `GOOGL-20260824-345-C` at 2.50:

```
Strike order v1
account: 0x58da990a8f4a3a6ca7cb6315d68a140105917352
series: GOOGL-20260824-345-C
side: buy
qty: 1000000
price: 2500000
nonce: 1755800000123
```

Signed with the **session** key:

```
0x2631931496eee605f3b2a789f83fc4f10dfec535cb6a2f7a21ce0fb2cf2b061a
  4f1afb9347d45b85d02b767405c905671b3bf1a92bfd23f91f47c5b53a5ee6f01c
```

Note `qty: 1000000` for one contract and `price: 2500000` for $2.50. If your signature differs, check your micro conversion before anything else.

### Vector 3 — a withdrawal

$250, signed with the **session** key:

```
Strike withdraw v1
account: 0x58da990a8f4a3a6ca7cb6315d68a140105917352
amount: 250000000
nonce: 1755800000123
```

```
0xd0446407fea9aeb7cf0518becf3982bb344cb6ed3e50b48cbfb611b784f47aa7
  2d89b12ec28f5c051bddd2483d019711aa6075712ec10a94d9ee07c69b790baf1b
```

### Vector 4 — cancel and margin

For account `0xabc0000000000000000000000000000000000001` at the same nonce. Note that `["META", "AAPL"]` sorts to `AAPL,META`:

```
Strike cancel v1
account: 0xabc0000000000000000000000000000000000001
symbols: AAPL,META
nonce: 1755800000123

Strike cancel v1
account: 0xabc0000000000000000000000000000000000001
symbols: all
nonce: 1755800000123

Strike margin v1
account: 0xabc0000000000000000000000000000000000001
enabled: true
nonce: 1755800000123
```

## Verifying your implementation

Check the envelope strings first, then the signatures. A mismatch in the strings is a formatting bug; a mismatch in the signatures with matching strings is a key-derivation or recovery-id bug.

Any EIP-191 `personal_sign` implementation reproduces them. With `eth-account` (`pip install eth-account`):

```bash
python - <<'PY'
from eth_account import Account
from eth_account.messages import encode_defunct

def sign_personal(key: bytes, envelope: str) -> str:
    return Account.sign_message(encode_defunct(text=envelope), key).signature.hex()

wallet_key  = bytes([0x09]) * 32
session_key = bytes([0x11]) * 32

approval = (
    "Strike: approve a trading session.\n\n"
    "This lets this device place orders on your behalf until it expires.\n"
    "It cannot withdraw funds, move positions, or approve another session.\n\n"
    "Account: 0x58da990a8f4a3a6ca7cb6315d68a140105917352\n"
    "Session key: 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a\n"
    "Expires: 1756404800000"
)
assert sign_personal(wallet_key, approval).endswith("5a881c")

order = (
    "Strike order v1\n"
    "account: 0x58da990a8f4a3a6ca7cb6315d68a140105917352\n"
    "series: GOOGL-20260824-345-C\n"
    "side: buy\n"
    "qty: 1000000\n"
    "price: 2500000\n"
    "nonce: 1755800000123"
)
assert sign_personal(session_key, order).endswith("ee6f01c")

withdraw = (
    "Strike withdraw v1\n"
    "account: 0x58da990a8f4a3a6ca7cb6315d68a140105917352\n"
    "amount: 250000000\n"
    "nonce: 1755800000123"
)
assert sign_personal(session_key, withdraw).endswith("790baf1b")
print("signer agrees with the venue")
PY
```

The venue's own test suite asserts these same vectors, so a drift between these pages and the engine goes red in CI rather than in production.

## Sending a signed request

Three fields travel in the JSON body of `/v1/order`, `/v1/cancel`, `/v1/margin` and `/v1/withdraw`, alongside the operation's own fields:

```json
{
  "seriesId": "BTC-1786492800-C-95000",
  "side": "buy",
  "qty": 1,
  "price": 1250.0,

  "sessionKey": "0x19e7…ff2a",
  "nonce": 1755800000123,
  "signature": "0x2631…f01c"
}
```

The presence of `sessionKey` is what selects the signed path. Send it and the signature must verify. Omit it and the request falls back to the bearer token — which is refused outright when signatures are mandatory.

Note that the body carries `qty` and `price` as **decimals** while the signature covers **micros**. The venue converts the body itself and rebuilds the envelope from its own micros, so a rounding disagreement fails the signature loudly instead of mis-pricing the order.

## Nonces

A nonce is a millisecond timestamp. It must be within **60,000 ms of venue time in either direction** — a fast clock fails as surely as a slow one — and must be unique per key inside that window. Both boundaries are inclusive.

A client placing several orders in one millisecond must step the nonce forward rather than reuse it, or the second is refused as a replay. Step it **boundedly**: running unboundedly ahead of the clock trades a replay bug for a liveness bug, because a nonce more than the window ahead is refused as stale.

{% hint style="info" %}
**Failure modes worth telling apart**

A bad signature does **not** burn the nonce it claimed, so an attacker who can see your nonces cannot lock you out by spending them with garbage. Retrying with a good signature on the same nonce works.

A `429` with `too many orders in flight — retry` is backpressure, not a rate limit and not a credential problem: the key spent more nonces inside the window than the venue will remember. Retry. Do **not** re-approve the session — nothing is wrong with it.
{% endhint %}

All signing faults are `401` and read alike on the wire, so carry the venue's own message through verbatim rather than rewording it. `nonce outside the accepted window` is a clock problem and `bad signature` is a code problem; collapsing them costs you that distinction. [Errors and limits](/api-reference/errors.md) lists the full set.

## Listing and revoking keys

```bash
curl -s "https://api.strikelabs.app/v1/auth/session?address=0xYourAddress"
```

Returns live keys only, soonest expiry first, each with `sessionKey`, `expiresAt` and `approvedAt`.

Revoking takes the **same proof as approving**: a wallet signature over the exact approval the key was granted under, including its original `expiresAt`. A bearer token alone is not enough, because revoking is destructive to someone's live trading. Read the current `expiresAt` from the listing above and rebuild the approval text with it.

```bash
curl -s -X POST https://api.strikelabs.app/v1/auth/session/revoke \
  -H 'content-type: application/json' \
  -d '{"address":"0x…","sessionKey":"0x…","signature":"0x…"}'
```

A revoked key is refused immediately. Re-approving an existing key resets its expiry and its replay memory.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.strikelabs.app/api-reference/signing.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
