認証と署名
書き込み操作に必要なEIP-712署名の要件です。
本番環境ではチェーンID 999 を使用してください。テストネットは、Hypercallが追加のテストネットHYPEを確保するまで一時的に停止しています。
概要
すべての書き込み操作にはEIP-712型付きデータ署名が必要です。署名者は次のいずれかでなければなりません。
- ウォレットアドレスそのもの(直接署名)、または
- そのウォレットの認可済みエージェント(エージェント認証を参照)
EIP-712ドメイン
ドメインセパレータ:
{
"name": "Hypercall",
"version": "1",
"chainId": 999,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
チェーンID: 999(本番環境)
メッセージタイプ
PlaceOrder
ルートを明示する場合の構造体:
struct PlaceOrder {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string route;
string clientId;
uint64 nonce;
}
ルートを省略する場合の構造体:
struct PlaceOrder {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string clientId;
uint64 nonce;
}
フィールド要件:
wallet: ウォレットアドレス(0xプレフィックス付きの16進文字列)symbol: オプションのシンボル(例:"BTC-20250131-100000-C")side:"Buy"または"Sell"(文字列の完全一致が必要)size: 文字列で指定するサイズ(例:"0.1")。JSONで送信する値と完全に一致させる必要がありますprice: 文字列で指定する価格(例:"100.0")。JSONで送信する値と完全に一致させる必要がありますtif: 有効期間条件(Time-in-force)。"gtc"、"ioc"、"fok"のいずれか(文字列の完全一致が必要)route: 任意の注文ルート。"best_execution"または"book_only"(指定する場合は文字列の完全一致が必要)。POST /bulk_orderは"book_only"のみを受け付けます。"rfq_only"は署名可能な値に含まれますが、現在はすべての注文エンドポイントで拒否されます。RFQで執行する場合はRFQエンドポイントを使用してください。clientId: クライアントが指定する注文ID(文字列。空文字列""も可)nonce: ミリ秒タイムスタンプのノンス(uint64)。署名者ごとに一意である必要があります。ノンス管理を参照してください
重要: price と size は、署名メッセージとJSONリクエストボディの両方で文字列でなければなりません。文字列のフォーマットも完全に一致させる必要があります。
ルートのデフォルト: route は送信してください。省略した注文も引き続き受け付けられ best_execution としてルーティングされますが、POST /order は Deprecation、Sunset、Warning ヘッダーを返します。公表済みの廃止日(2026年7月4日)はすでに過ぎています。ルート省略形式は追加の猶予期間なしに削除される可能性があります。JSONから route を省略する場合は、型付きデータからも省略してください。
PlaceOrderReduceOnly
リデュースオンリー注文(JSONボディで "reduce_only": true)は、別の構造体に署名します。この場合は route の明示が必須です。
struct PlaceOrderReduceOnly {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string route;
string clientId;
bool reduceOnly;
uint64 nonce;
}
reduceOnly は true で、リクエストボディと一致している必要があります。route に署名していないリデュースオンリー注文は missing_field で拒否されます。
ReplaceOrder
PUT /order と PUT /bulk_order の各エントリは ReplaceOrder に署名します。この構造体には route フィールドがなく、JSONボディ内の route キーは無視されます。
struct ReplaceOrder {
address wallet;
string orderId;
string symbol;
string side;
string size;
string price;
string tif;
string clientId;
uint64 nonce;
}
orderId は置き換え対象の注文IDを10進数の文字列で表したものです。リデュースオンリーの置き換え("reduce_only": true)は ReplaceOrderReduceOnly に署名します。これは clientId と nonce の間に bool reduceOnly を追加した構造体です。TypeScript SDKは両方の型マップをエクスポートしています。
CancelOrder
構造体:
struct CancelOrder {
address wallet;
string orderId;
uint64 nonce;
}
フィールド要件:
wallet: ウォレットアドレスorderId: 文字列で指定する注文ID(例:"123")nonce: 一意のノンス
CancelOrderByClientId
構造体:
struct CancelOrderByClientId {
address wallet;
string clientId;
uint64 nonce;
}
フィールド要件:
wallet: ウォレットアドレスclientId: クライアント注文ID(文字列)nonce: 一意のノンス
ApproveAgent
構造体:
struct ApproveAgent {
address agent;
uint64 nonce;
}
フィールド要件:
agent: 認可するエージェントのウォレットアドレスnonce: 一意のノンス
注: エージェントを認可するウォレットは、署名から復元されたアドレスによって決まります。
RevokeAgent
構造体:
struct RevokeAgent {
address agent;
uint64 nonce;
}
フィールド要件:
agent: 取り消すエージェントのウォレットアドレスnonce: 一意のノンス
RevokeAllAgents
DELETE /revoke-all-agents は、署名したウォレットのエージェントをすべて削除します。
struct RevokeAllAgents {
uint64 nonce;
}
リスクシミュレーション
POST /risk/simulate/portfolio と POST /risk/simulate/orders は、公開された読み取り専用のエンジン権限リクエストです。署名、認証、ノンスはいずれも不要です。wallet フィールドで評価対象のアカウントスナップショットを指定します。
ポートフォリオリクエストの例:
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"}
]
}'
アトミック注文リクエストの例:
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 は任意です。各エントリはオーダーブックに残るGTC注文として扱われ、orders より前に配列順に1件ずつ受け入れられます。このリクエストに限り、アカウントの正規のライブ未約定注文および有効なリザベーションと合わせて評価されます。エンジンの状態は変更されません。シミュレーションが成功すると、simulated と delta には仮想注文のシーケンス全体に加え、提案された指値注文またはアトミックパッケージが含まれます。2つの注文配列の合計エントリ数は最大50件です。
シミュレーションのレスポンスはスナップショットであり、執行を保証するものではありません。ルーティング、RPIの可用性、マッチング、ノンスの受け入れ、将来の取引停止状態はシミュレーションの対象外です。エンジンの状態が進んだ後に送信する実際の注文では、結果が異なる場合があります。
署名プロセス
ステップ1: メッセージを構築する
ルートを明示した PlaceOrder の例:
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
};
ステップ2: ドメインを定義する
const domain = {
name: "Hypercall",
version: "1",
chainId: 999,
verifyingContract: "0x0000000000000000000000000000000000000000"
};
ステップ3: 型付きデータに署名する
ethers.js v6を使用する場合(v5ではメソッド名が _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);
ステップ4: リクエストを送信する
重要: JSONリクエストボディでは、price と 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
};
ノンス管理
要件:
- ノンスは、サーバー時刻(ミリ秒)の2日前から1日後までの範囲内(境界を含まない)に収まっている必要があります。そのため、現在のUnix時刻をミリ秒で使用してください。
1や123のような小さなカウンター値は拒否されます。 - ノンスは署名鍵ごとに管理されます。注文とキャンセルでは、エージェントが署名した場合はそのエージェントが対象です。エージェントの承認と取り消しでは、オーナーのウォレットが対象です。
- ノンスは再利用できません。注文のノンスは
POST /order、一括注文、WebSocketのPlaceOrderで共有されるため、ある経路で使用したノンスを別の経路で再利用することはできません。 - ノンスの欠番は許容されます。エンジンは署名者ごとに直近100件のノンスを保持し、100件に達した後は、新しいノンスが保持中の最小値より大きくなければなりません。
ベストプラクティス:
- ミリ秒単位のエポック時刻を起点とし、署名者ごとに最後のノンスを永続化して
max(now_ms, last + 1)を返すようにします - 同じ鍵で署名するすべてのプロセスで、1つのアロケーターを共有します
- リクエストが失敗した場合は、新しいノンスで再署名します
エージェント認可
エージェントウォレットで署名する場合:
-
エージェントを承認する(初回のみ):
POST /approve-agent{"agent": "0x...","nonce": 1791273600000,"signature": "0x..." # Signed by wallet owner} -
エージェントウォレットで注文に署名する:
- エージェントウォレットで
PlaceOrder/CancelOrderメッセージに署名します walletフィールドには取引用ウォレットのアドレスを設定します- ミドルウェアが、そのウォレットに対してエージェントが認可されていることを検証します
- エージェントウォレットで
エージェント認可の詳細はエージェント認証を参照してください。
パーペチュアル注文
パーペチュアル注文には、上記の Hypercall ドメインは使用しません。Hypercallアカウントに登録されたAPIウォレットが、HypercallApiSign ドメイン(チェーンID 999、検証コントラクトはゼロアドレス)で HLOrder として署名します。構造体と単位についてはEIP-712署名を参照してください。
APIでは、TEEルートはオプトイン方式です。TEEエンドポイントを呼び出すことで選択します。Hypercallアプリではこちらがデフォルトです。利用するには、署名済みの注文を account、nonce、action、signature とともに POST /v1/tee-perp/orders/submit に送信します。Hypercallが証拠金チェックを行った後、セキュアエンクレーブがお客様のアカウント用のネイティブHyperliquid注文に署名して送信します。すべてのパーペチュアル注文には手数料0%のHypercallビルダーコードが付与されるため、最初の注文の前にアカウントのマネージャーがそのビルダーを一度承認する必要があります(アプリの取引を有効化ステップ)。承認していない場合、注文は builder_not_approved で拒否されます。フローについてはパーペチュアルを、リクエストとレスポンスのスキーマについてはAPIリファレンスを参照してください。
Hyperliquid独自の Exchange ドメイン(チェーンID 1337、MessagePackの Agent ペイロード)は、Hypercall APIでは受け付けられません。
よくあるエラー
unauthorized(HTTP 401): "Signer 0x... is not authorized to act for wallet 0x..."
EIP-712の復元処理は、常に何らかのアドレスを返します。署名した内容と送信した内容が少しでも異なると、復元されたアドレスはお客様の鍵のものにならず、リクエストは署名エラーではなくこのエラーで失敗します。
原因:
- 署名時と送信時で
priceまたはsizeの文字列フォーマットが変わっている(例:"100.0"と"100") - 署名したフィールドがJSONボディと異なる(
route、clientId/client_id、nonce、tif)。またはrouteに署名したのにボディで省略している、あるいはその逆 - ドメインが誤っている(チェーンID、名前、バージョン、検証コントラクト)
- リクエストに対して構造体が誤っている(例:
PUT /orderでReplaceOrderではなくPlaceOrderを使用) - 署名者が本当にウォレット本人でも承認済みエージェントでもない。
POST /approve-agentでエージェントを承認するか、ウォレットで直接署名してください
メッセージに見覚えのない署名者アドレスが表示されている場合は、原因1から4のいずれかです。
signature_verification_failed(HTTP 400)
原因: 署名をデコードまたは復元できませんでした。たとえば長さが誤っている、16進数として無効である、などです。
invalid_parameter(HTTP 400): "price must be provided as a string for signature verification"
原因: price または size がJSONの数値として送信されました。どちらも文字列で送信してください。
実装例
Python(eth_account 0.10以降)
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);
署名のテスト
署名生成をテストする手順の例:
- 実装した処理で署名を生成します
POST /orderでテスト注文を送信します- レスポンスを確認します。HTTP 200で
statusが"ACKED"、またはreason付きの"REJECTED"になります - HTTP 401
unauthorizedで見覚えのない署名者アドレスが返された場合、またはHTTP 400signature_verification_failedが返された場合は、次を確認してください。priceとsizeの文字列フォーマット- ドメインパラメータ(特に
chainId) routeとnonceを含め、署名したすべてのフィールドがボディと一致していること
セキュリティ上の考慮事項
- 秘密鍵を絶対に公開しない: ハードウェアウォレットまたは安全な鍵管理を使用してください
- ノンス管理: 署名鍵ごとに、永続化したミリ秒タイムスタンプのノンスを使用してください
- エージェント認可:
GET /authorized-agentsで認可済みエージェントを定期的に監査してください - 文字列エンコーディング: 署名とリクエストの両方で
priceとsizeが文字列であることを確認してください
参考資料
- EIP-712: https://eips.ethereum.org/EIPS/eip-712
- 実装: 署名復元とミドルウェアスタックで処理されます。