# Sign In With a Wallet

> Sign in to the WakeTrades API with a message signed by the agent's wallet (EIP-191). The first sign-in creates the account. No API key, email or password.

The agent proves it controls a wallet by signing a short text message. The first sign-in of a wallet creates its WakeTrades account.

## Steps

1. `POST /api/v1/session/nonce` with `{ "address": "0x…" }`. The answer holds a `nonce` (valid 10 minutes) and the exact `message` to sign.
2. Sign `message` as is with the wallet (EIP-191 `personal_sign`).
3. `POST /api/v1/session/verify` with `{ "address", "nonce", "message", "signature" }`. The answer holds a session `token` (on the site that issued it only) and the account. From a script or agent (no `Origin` header), the token is valid until your plan or free trial ends, 6 months at most, and 7 days before the trial starts; paying extends it without a new sign-in. Signed in on the website, it lasts 7 days. Tokens issued before 13 October 2026 no longer work: sign in again once.
4. Send the token on every other request: `Authorization: Bearer <token>`.

**Helping a person who keeps their wallet?** Don't sign in: they connect you from **Profile › AI Agent** on waketrades.com and give you a code. Use it as the token (`Authorization: Bearer <code>`). It works until their plan or free trial ends, 180 days at most, and stops at once if they disconnect you or their plan stops (`401 agent_code_expired`: ask them for a new code). A View only code can't change anything: `403 view_only` ([Connect Your AI Agent](https://waketrades.com/docs/account/connect-ai-agent.md)).

`POST /api/v1/session/revoke-all` signs out every session of the wallet (browsers, agents, links opened from the Telegram bot), the one that calls it included. Use it if a token may have leaked; copying is not affected. On the website: **Profile › Sign out of all sessions**.

The message only signs you in: it places no order and grants no permission. It follows the Sign-In with Ethereum standard (EIP-4361), so wallets check that the site asking for it is waketrades.com. Sign it exactly as returned, without changing a character. It looks like this:

```text
waketrades.com wants you to sign in with your Ethereum account:
0x1234…

Sign-in only. No order will be placed and no permission is granted.

URI: https://waketrades.com
Version: 1
Chain ID: 42161
Nonce: …
Issued At: 2026-10-10T12:00:00.000Z
Expiration Time: 2026-10-10T12:10:00.000Z
```

## Python

```python
import requests
from eth_account import Account
from eth_account.messages import encode_defunct

BASE = "https://waketrades.com/api/v1"
acct = Account.from_key(PRIVATE_KEY)

n = requests.post(f"{BASE}/session/nonce", json={"address": acct.address}).json()
sig = "0x" + bytes(acct.sign_message(encode_defunct(text=n["message"])).signature).hex()
s = requests.post(f"{BASE}/session/verify", json={"address": acct.address, "nonce": n["nonce"], "message": n["message"], "signature": sig}).json()
headers = {"Authorization": f"Bearer {s['token']}"}
```

## JavaScript (viem)

```js
import { privateKeyToAccount } from "viem/accounts";

const BASE = "https://waketrades.com/api/v1";
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const post = (path, body, token) => fetch(BASE + path, { method: "POST", headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify(body) }).then((r) => r.json());

const { nonce, message } = await post("/session/nonce", { address: account.address });
const signature = await account.signMessage({ message });
const { token } = await post("/session/verify", { address: account.address, nonce, message, signature });
```

## JavaScript Without a Wallet Library

No viem, ethers or eth-account at hand? Two small audited libraries are enough for every signature WakeTrades asks for: the sign-in message (EIP-191 `personal_sign`) and Hyperliquid's typed data (EIP-712) for the trading key and payments. Hyperliquid's typed data only uses flat `string`, `address` and `uint` fields, which keeps the encoder short. Tested with these exact versions (later major versions changed `sign`):

```js
// npm i @noble/curves@1.9.7 @noble/hashes@1.8.0 — save as sign.mjs
import { secp256k1 } from "@noble/curves/secp256k1.js";
import { keccak_256 } from "@noble/hashes/sha3.js";
import { bytesToHex, hexToBytes, utf8ToBytes, concatBytes } from "@noble/hashes/utils.js";

const KEY = hexToBytes(process.env.PRIVATE_KEY.replace(/^0x/, ""));
export const address = "0x" + bytesToHex(keccak_256(secp256k1.getPublicKey(KEY, false).slice(1)).slice(-20));

// { r, s, v }: r and s with 64 hex digits each, v = 27 or 28
function sign(hash) {
  const sig = secp256k1.sign(hash, KEY);   // deterministic (RFC 6979), low s
  const hex = (n) => "0x" + n.toString(16).padStart(64, "0");
  return { r: hex(sig.r), s: hex(sig.s), v: 27 + sig.recovery };
}

// EIP-191 personal_sign, for POST /session/verify: a 65-byte hex string
export function personalSign(message) {
  const m = utf8ToBytes(message);
  const { r, s, v } = sign(keccak_256(concatBytes(utf8ToBytes(`\x19Ethereum Signed Message:\n${m.length}`), m)));
  return r + s.slice(2) + v.toString(16);
}

// EIP-712 with flat types, for POST /agent/approve and POST /pay: { r, s, v }
const encode = (type, value) =>
  type === "string" ? keccak_256(utf8ToBytes(value))
  : type === "address" ? hexToBytes(value.slice(2).toLowerCase().padStart(64, "0"))
  : hexToBytes(BigInt(value).toString(16).padStart(64, "0"));   // uint64, uint256
const hashStruct = (name, fields, data) => keccak_256(concatBytes(
  keccak_256(utf8ToBytes(`${name}(${fields.map((f) => `${f.type} ${f.name}`).join(",")})`)),
  ...fields.map((f) => encode(f.type, data[f.name]))));
const DOMAIN = [{ name: "name", type: "string" }, { name: "version", type: "string" },
  { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" }];
export function signTypedData({ domain, types, primaryType, message }) {
  return sign(keccak_256(concatBytes(new Uint8Array([0x19, 0x01]),
    hashStruct("EIP712Domain", DOMAIN, domain), hashStruct(primaryType, types[primaryType], message))));
}
```

Use it like this:

```js
import { address, personalSign, signTypedData } from "./sign.mjs";
// Sign-in: { address, nonce, message, signature: personalSign(message) }
// Trading key (Set Up Copying, step 3): { action, signature: signTypedData(typedData) }
// Payment (GET /pay/prepare returns action and typedData): { plan, action, signature: signTypedData(typedData) }
```

Our test suite runs this exact code against the server's own signature checks on every release.

## Errors

| Code | Meaning |
| --- | --- |
| `invalid_address` | The address isn't a valid Ethereum address. |
| `invalid_nonce` | The nonce is wrong, older than 10 minutes or already used (one sign-in per signature). Ask for a new one. |
| `invalid_message` | The signed text isn't the one returned with the nonce. |
| `wrong_signer` | The signature comes from another wallet. |
| `origin_not_allowed` | A browser page on another website tried to finish the sign-in. Sign in from waketrades.com, or from a script or agent. |
| `signup_limit` | Too many new accounts from this IP address today (3 per day). |
| `session_required` | Missing or expired token on a protected route. Sign in again. |
| `agent_code_expired` | The code from Profile › AI Agent has ended (180 days, plan end, or plan stopped). Ask the person for a new one. |
