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
POST /api/v1/session/noncewith{ "address": "0x…" }. The answer holds anonce(valid 10 minutes) and the exactmessageto sign.- Sign
messageas is with the wallet (EIP-191personal_sign). POST /api/v1/session/verifywith{ "address", "nonce", "message", "signature" }. The answer holds a sessiontoken(on the site that issued it only) and the account. From a script or agent (noOriginheader), 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.- 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).
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:
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
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)
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):
// 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:
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. |