Autenticação e assinatura
Requisitos de assinatura EIP-712 para operações de escrita.
Use o chain ID 999 em produção. A testnet está temporariamente desativada até que a Hypercall obtenha mais HYPE de testnet.
Visão geral
Todas as operações de escrita exigem assinaturas de dados tipados EIP-712. O signatário deve:
- Ser o próprio endereço da carteira (assinatura direta), OU
- Ser um agente autorizado para a carteira (veja Autenticação de agentes)
Domínio EIP-712
Domain separator:
{
"name": "Hypercall",
"version": "1",
"chainId": 999,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
Chain ID: 999 (produção)
Tipos de mensagem
PlaceOrder
Struct com route explícito:
struct PlaceOrder {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string route;
string clientId;
uint64 nonce;
}
Struct quando route é omitido:
struct PlaceOrder {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string clientId;
uint64 nonce;
}
Requisitos dos campos:
wallet: endereço da carteira (string hexadecimal com prefixo 0x)symbol: símbolo da opção (ex.:"BTC-20250131-100000-C")side:"Buy"ou"Sell"(correspondência exata da string obrigatória)size: tamanho como string (ex.:"0.1"). DEVE corresponder exatamente ao que é enviado no JSONprice: preço como string (ex.:"100.0"). DEVE corresponder exatamente ao que é enviado no JSONtif: time-in-force:"gtc","ioc"ou"fok"(correspondência exata da string obrigatória)route: rota da ordem opcional:"best_execution"ou"book_only"(correspondência exata da string obrigatória quando presente).POST /bulk_orderaceita apenas"book_only"."rfq_only"faz parte do vocabulário assinado, mas todos os endpoints de ordem o rejeitam atualmente; use os endpoints de RFQ para execução via RFQ.clientId: ID da ordem fornecido pelo cliente (string, pode ser vazio"")nonce: nonce baseado em timestamp em milissegundos (uint64), único por signatário. Veja Gerenciamento de nonce
Crítico: price e size DEVEM ser strings tanto na mensagem assinada quanto no corpo JSON da requisição. A formatação das strings deve corresponder exatamente.
Route padrão: envie route. Uma ordem sem ele ainda é aceita e roteada como best_execution, mas POST /order responde com os headers Deprecation, Sunset e Warning, e a data de sunset publicada (4 de julho de 2026) já passou. A forma sem route pode ser removida sem novo período de carência. Se você omitir route do JSON, omita-o também dos dados tipados.
PlaceOrderReduceOnly
Uma ordem reduce-only ("reduce_only": true no corpo JSON) assina uma struct separada. Ela exige um route explícito.
struct PlaceOrderReduceOnly {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string route;
string clientId;
bool reduceOnly;
uint64 nonce;
}
reduceOnly deve ser true e corresponder ao corpo da requisição. Uma ordem reduce-only sem route assinado é rejeitada com missing_field.
ReplaceOrder
PUT /order e cada entrada de PUT /bulk_order assinam um ReplaceOrder. Ele não tem campo route; uma chave route no corpo JSON é ignorada.
struct ReplaceOrder {
address wallet;
string orderId;
string symbol;
string side;
string size;
string price;
string tif;
string clientId;
uint64 nonce;
}
orderId é a string decimal da ordem que está sendo substituída. Uma substituição reduce-only ("reduce_only": true) assina ReplaceOrderReduceOnly, que adiciona bool reduceOnly entre clientId e nonce. O SDK TypeScript exporta os dois mapas de tipos.
CancelOrder
Struct:
struct CancelOrder {
address wallet;
string orderId;
uint64 nonce;
}
Requisitos dos campos:
wallet: endereço da carteiraorderId: ID da ordem como string (ex.:"123")nonce: nonce único
CancelOrderByClientId
Struct:
struct CancelOrderByClientId {
address wallet;
string clientId;
uint64 nonce;
}
Requisitos dos campos:
wallet: endereço da carteiraclientId: ID da ordem do cliente (string)nonce: nonce único
ApproveAgent
Struct:
struct ApproveAgent {
address agent;
uint64 nonce;
}
Requisitos dos campos:
agent: endereço da carteira do agente a ser autorizadononce: nonce único
Observação: a carteira que autoriza o agente é derivada da assinatura recuperada.
RevokeAgent
Struct:
struct RevokeAgent {
address agent;
uint64 nonce;
}
Requisitos dos campos:
agent: endereço da carteira do agente a ser revogadononce: nonce único
RevokeAllAgents
DELETE /revoke-all-agents remove todos os agentes da carteira signatária.
struct RevokeAllAgents {
uint64 nonce;
}
Simulação de risco
POST /risk/simulate/portfolio e POST /risk/simulate/orders são requisições
públicas, somente leitura, com autoridade do engine. Elas não exigem assinatura,
autenticação nem nonce. O campo wallet seleciona o snapshot da conta a ser avaliado.
Exemplo de requisição de portfólio:
curl -X POST https://api.hypercall.xyz/risk/simulate/portfolio \
-H 'Content-Type: application/json' \
-d '{
"wallet": "0x1111111111111111111111111111111111111111",
"add_positions": true,
"positions": [
{"symbol": "BTC-20261231-100000-C", "quantity": "1.25"}
]
}'
Exemplo de requisição de ordens atômicas:
curl -X POST https://api.hypercall.xyz/risk/simulate/orders \
-H 'Content-Type: application/json' \
-d '{
"wallet": "0x1111111111111111111111111111111111111111",
"execution_mode": "atomic",
"hypothetical_open_orders": [
{"symbol": "BTC-PERP", "side": "Buy", "size": "0.1", "price": "100000", "reduce_only": false}
],
"orders": [
{"symbol": "BTC-20261231-100000-C", "side": "Buy", "size": "1", "price": "4200", "reduce_only": false},
{"symbol": "BTC-20261231-120000-C", "side": "Sell", "size": "1", "price": "1800", "reduce_only": false}
]
}'
hypothetical_open_orders é opcional. Suas entradas são tratadas como ordens GTC
em repouso no livro e admitidas sequencialmente na ordem do array, antes de orders.
Apenas nesta requisição, elas são avaliadas junto com as ordens abertas ativas e as
reservas ativas da conta, conforme o estado autoritativo. O estado do engine não é
modificado. Em uma simulação bem-sucedida, simulated e delta incluem toda a
sequência hipotética mais a ordem em repouso proposta ou o pacote atômico. Os dois
arrays de ordens podem conter no máximo 50 entradas no total.
As respostas de simulação são snapshots, não garantias de execução. Roteamento, disponibilidade de RPI, matching, admissão de nonce e estado futuro de suspensão de negociação não são simulados. Uma ordem real posterior pode ter resultado diferente depois que o estado do engine avançar.
Processo de assinatura
Passo 1: construir a mensagem
Exemplo de PlaceOrder com route explícito:
const message = {
wallet: "0x1111111111111111111111111111111111111111",
symbol: "BTC-20250131-100000-C",
side: "Buy",
size: "0.1", // MUST be string
price: "100.0", // MUST be string
tif: "gtc",
route: "best_execution",
clientId: "mm-1",
nonce: Date.now() // current ms epoch; send this exact value in the body
};
Passo 2: definir o domínio
const domain = {
name: "Hypercall",
version: "1",
chainId: 999,
verifyingContract: "0x0000000000000000000000000000000000000000"
};
Passo 3: assinar os dados tipados
Usando ethers.js v6 (na v5 o método é _signTypedData):
const signature = await signer.signTypedData(domain, {
PlaceOrder: [
{ name: "wallet", type: "address" },
{ name: "symbol", type: "string" },
{ name: "side", type: "string" },
{ name: "size", type: "string" },
{ name: "price", type: "string" },
{ name: "tif", type: "string" },
{ name: "route", type: "string" },
{ name: "clientId", type: "string" },
{ name: "nonce", type: "uint64" }
]
}, message);
Passo 4: enviar a requisição
Crítico: o corpo JSON da requisição deve usar exatamente os mesmos valores de string para price e size:
const body = {
wallet: message.wallet,
symbol: message.symbol,
price: message.price, // same string as signed
size: message.size, // same string as signed
side: message.side,
tif: message.tif,
route: message.route,
client_id: message.clientId,
nonce: message.nonce, // same value as signed
signature
};
Gerenciamento de nonce
Requisitos:
- Os nonces devem estar estritamente dentro do intervalo de dois dias antes e um dia depois do horário do servidor em milissegundos, então use o tempo Unix atual em milissegundos. Contadores pequenos como
1ou123são rejeitados. - Os nonces são rastreados por chave de assinatura. Para ordens e cancelamentos, é o agente quando um agente assina; para aprovação e revogação de agentes, é a carteira titular.
- Um nonce não pode ser reutilizado. Os nonces de ordens são compartilhados entre
POST /order, ordens em lote ePlaceOrdervia WebSocket; um nonce usado em um transporte não pode ser reutilizado em outro. - Lacunas são permitidas. O engine mantém os 100 nonces mais recentes por signatário e, quando já guarda 100, um novo nonce deve ser maior que o menor deles.
Boas práticas:
- Inicialize a partir do tempo epoch em milissegundos e persista o último nonce por signatário, retornando
max(now_ms, last + 1) - Compartilhe um único alocador entre todos os processos que assinam com a mesma chave
- Se uma requisição falhar, assine-a novamente com um novo nonce
Autorização de agentes
Se você usar uma carteira de agente para assinar:
-
Aprove o agente (uma única vez):
POST /approve-agent{"agent": "0x...","nonce": 1791273600000,"signature": "0x..." # Signed by wallet owner} -
Assine ordens com a carteira do agente:
- Use a carteira do agente para assinar mensagens
PlaceOrder/CancelOrder - Defina o campo
walletcom o endereço da carteira de negociação - O middleware verifica se o agente está autorizado para essa carteira
- Use a carteira do agente para assinar mensagens
Veja Autenticação de agentes para todos os detalhes sobre autorização de agentes.
Ordens de perpétuos
Ordens de contratos perpétuos não usam o domínio Hypercall acima. Elas são assinadas como HLOrder no domínio HypercallApiSign (chain ID 999, verifying contract zerado) por uma API wallet registrada na sua Hypercall Account. Veja Assinatura EIP-712 para a struct e as unidades.
A rota TEE é opcional na API: você a escolhe chamando os endpoints TEE. No app da Hypercall, ela é o padrão. Para usá-la, envie a ordem assinada para POST /v1/tee-perp/orders/submit com account, nonce, action e signature. A Hypercall executa a verificação de margem e, em seguida, um enclave seguro assina e envia a ordem nativa da Hyperliquid para a sua Account. Toda ordem de perpétuo carrega o builder code da Hypercall com taxa de 0%, então o manager da Account precisa aprovar esse builder uma vez antes da primeira ordem (a etapa Enable trading do app). Sem isso, a ordem é rejeitada com builder_not_approved. Veja Perpétuos para o fluxo e a Referência da API para o schema de requisição e resposta.
O domínio Exchange da própria Hyperliquid (chain ID 1337, payloads Agent em MessagePack) não é aceito pela API da Hypercall.
Erros comuns
unauthorized (HTTP 401): "Signer 0x... is not authorized to act for wallet 0x..."
A recuperação EIP-712 sempre produz algum endereço. Se qualquer coisa que você assinou diferir do que você enviou, o endereço recuperado não será o da sua chave, e a requisição falha aqui, e não com um erro de assinatura.
Causas:
- A formatação da string de
priceousizemudou entre a assinatura e o envio (ex.:"100.0"vs"100") - Um campo assinado difere do corpo JSON (
route,clientId/client_id,nonce,tif), ouroutefoi assinado mas omitido do corpo, ou o inverso - Domínio errado (chain ID, name, version, verifying contract)
- Struct errada para a requisição (por exemplo,
PlaceOrderem vez deReplaceOrderemPUT /order) - O signatário de fato não é nem a carteira nem um agente aprovado. Aprove o agente via
POST /approve-agentou assine diretamente com a carteira
Quando a mensagem cita um endereço de signatário que você não reconhece, a causa é uma das causas 1 a 4.
signature_verification_failed (HTTP 400)
Causa: a assinatura não pôde ser decodificada nem recuperada, por exemplo, por tamanho errado ou hex inválido.
invalid_parameter (HTTP 400): "price must be provided as a string for signature verification"
Causa: price ou size foi enviado como número JSON. Envie ambos como strings.
Exemplos de implementação
Python (eth_account 0.10 ou posterior)
import time
from eth_account import Account
from eth_account.messages import encode_typed_data
domain = {
"name": "Hypercall",
"version": "1",
"chainId": 999,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
message = {
"wallet": "0x1111111111111111111111111111111111111111",
"symbol": "BTC-20250131-100000-C",
"side": "Buy",
"size": "0.1",
"price": "100.0",
"tif": "gtc",
"route": "best_execution",
"clientId": "mm-1",
"nonce": int(time.time() * 1000) # current ms epoch; send this exact value in the body
}
types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"PlaceOrder": [
{"name": "wallet", "type": "address"},
{"name": "symbol", "type": "string"},
{"name": "side", "type": "string"},
{"name": "size", "type": "string"},
{"name": "price", "type": "string"},
{"name": "tif", "type": "string"},
{"name": "route", "type": "string"},
{"name": "clientId", "type": "string"},
{"name": "nonce", "type": "uint64"}
]
}
structured_msg = {
"types": types,
"domain": domain,
"primaryType": "PlaceOrder",
"message": message
}
encoded = encode_typed_data(full_message=structured_msg)
signed = Account.sign_message(encoded, private_key)
signature = signed.signature.hex()
JavaScript (ethers.js v6)
const { ethers } = require("ethers");
const domain = {
name: "Hypercall",
version: "1",
chainId: 999,
verifyingContract: "0x0000000000000000000000000000000000000000"
};
const types = {
PlaceOrder: [
{ name: "wallet", type: "address" },
{ name: "symbol", type: "string" },
{ name: "side", type: "string" },
{ name: "size", type: "string" },
{ name: "price", type: "string" },
{ name: "tif", type: "string" },
{ name: "route", type: "string" },
{ name: "clientId", type: "string" },
{ name: "nonce", type: "uint64" }
]
};
const message = {
wallet: "0x1111111111111111111111111111111111111111",
symbol: "BTC-20250131-100000-C",
side: "Buy",
size: "0.1",
price: "100.0",
tif: "gtc",
route: "best_execution",
clientId: "mm-1",
nonce: Date.now() // current ms epoch; send this exact value in the body
};
const signature = await signer.signTypedData(domain, types, message);
Testando assinaturas
Exemplo para testar a geração de assinaturas:
- Gere a assinatura com a sua implementação
- Envie uma ordem de teste via
POST /order - Verifique a resposta: HTTP 200 com
status"ACKED", ou"REJECTED"com umreason - Se a requisição falhar com HTTP 401
unauthorizede um endereço de signatário desconhecido, ou com HTTP 400signature_verification_failed, verifique:- A formatação das strings de
priceesize - Os parâmetros do domínio (especialmente
chainId) - Se todos os campos assinados correspondem ao corpo, incluindo
routeenonce
- A formatação das strings de
Considerações de segurança
- Nunca exponha chaves privadas: use hardware wallets ou um gerenciamento de chaves seguro
- Gerenciamento de nonce: use nonces persistentes, baseados em timestamp em milissegundos, por chave de assinatura
- Autorização de agentes: audite regularmente os agentes autorizados via
GET /authorized-agents - Codificação de strings: garanta que
priceesizesejam strings tanto na assinatura quanto na requisição
Referências
- EIP-712: https://eips.ethereum.org/EIPS/eip-712
- Implementação: tratada pela pilha de recuperação de assinatura e middleware.