03 · Wallet & accounts
Wallet & accounts
Wallet, accounts and equity#
Your wallet is your own address. Your money outside the venue stays in your HyperCore account, and PORTFOLIO prints it on one line ("In your HyperCore wallet"). An account holds its own equity — the cash deposited into it plus its own profit and loss — and its own positions. Margin, the leverage cap and the solvency test read the account's equity and nothing else. There is no idle balance sitting between the two. A freshly connected wallet has no accounts; the act that needs one opens it.
Nothing moves out of your HyperCore wallet except an amount a button printed and you signed. A DEPOSIT or an ADD MARGIN moves dollars from your HyperCore wallet into an account, with the amount printed on the button. An open that would leave the account short says so, and its button carries the amount (ADD $X AND TRANSACT). Fees, funding and liquidations never reach into your wallet. WITHDRAW releases equity above what the account must keep behind what it holds, and is refused below that, with both numbers named. It goes back to your HyperCore wallet; daily limits are in section 6. When a single position closes, what it frees stays in the account; CLOSE ACCOUNT pays the whole residual back to your wallet.
Do this from your agent
Fund your wallet, open an account, put cash behind it
Testnet. You need USDC in your own HyperCore testnet account (Perps balance). Check it, then move some to the venue. One signature from your wallet; the venue forwards it and credits your ledger cash within a minute or two.
curl -s "https://protect.temporal.exchange/api/venue/hypercore/balance?wallet=0xYOUR_WALLET"{"ok":true,"wallet":"0x…","perpUSD":250,"spotUSD":0,"withdrawable":250,"cached":false}curl -s "https://protect.temporal.exchange/api/venue/chain/deposits?wallet=0xYOUR_WALLET"{"ok":true,"bank":"0x6e9d44…","network":"testnet","transfers":[]}// fund.mjs <dollars>: move USDC from your own HyperCore PERPS balance to the venue. The venue credits your ledger cash within a minute or two.
import { Signature } from "ethers";
import { wallet, get, post, until } from "./temporal.mjs";
const amount = Number(process.argv[2]);
const { bank, network } = await get(`/chain/deposits?wallet=${wallet.address}`); // the venue's HyperCore address, served, never hard-coded
const cashBefore = (await get(`/chain/state?wallet=${wallet.address}`)).cashUSD;
const time = Date.now(); // HyperCore's nonce is the time in milliseconds
const action = {
type: "usdSend",
hyperliquidChain: network === "mainnet" ? "Mainnet" : "Testnet",
signatureChainId: "0x66eee", // HyperCore signs every network on chain 421614
destination: bank,
amount: amount.toFixed(2),
time,
};
const domain = { name: "HyperliquidSignTransaction", version: "1", chainId: 0x66eee, verifyingContract: "0x0000000000000000000000000000000000000000" };
const types = {
"HyperliquidTransaction:UsdSend": [
{ name: "hyperliquidChain", type: "string" },
{ name: "destination", type: "string" },
{ name: "amount", type: "string" },
{ name: "time", type: "uint64" },
],
};
const { r, s, v } = Signature.from(await wallet.signTypedData(domain, types, { hyperliquidChain: action.hyperliquidChain, destination: bank, amount: action.amount, time }));
console.log(await post("/hypercore/relay", { action, nonce: time, signature: { r, s, v } })); // { ok: true, from, amountUSD, ... }
await until(() => get(`/chain/state?wallet=${wallet.address}`), (st) => st.cashUSD >= cashBefore + amount - 0.02, 240);
console.log("credited");The transfer is HyperCore's own usdSend to the address the venue serves in bank. Never type that address by hand: read it from the route. The venue refuses a transfer to any other address, and refuses a transfer signed for the wrong network, by name.
Move $50 of my HyperCore testnet USDC to the Temporal venue. First read my HyperCore balance and my Temporal cash and tell me both. Then show me the transfer you will sign (amount and destination) and wait for my yes. Use fund.mjs from https://protect.temporal.exchange/docs/wallet-and-accounts. Afterwards poll /chain/state until my cash rises by $50 and report it.
Check the result yourself: the money-trail check.
Do this from your agent
Open an account and deposit
The first act of a new wallet is connect() (once). An account is opened by mintAccount(kind): 0 is OPTIONS, 1 is PROTECT, 3 is EARN. deposit(account, usd) moves free cash into it. All three are signed intents; the script does them in order and waits for the venue to show each change.
// setup.mjs <trade|protect|earn> <dollars>: connect once, open an account of that kind, and move <dollars> of free cash into it.
import { wallet, get, call, act, until, usd, KIND } from "./temporal.mjs";
const [kind = "trade", dollars = "0"] = process.argv.slice(2);
const read = () => get(`/chain/state?wallet=${wallet.address}`);
const mine = (st) => Object.values(st.accounts ?? {}).filter((a) => a.kind === kind);
const before = await read();
if (!before.connected) console.log("connect:", await act(call("connect"))); // once per wallet
console.log("mint:", await act(call("mintAccount", KIND[kind])));
const after = await until(read, (st) => mine(st).length > mine(before).length);
const account = mine(after).sort((a, b) => Number(b.id) - Number(a.id))[0]; // the newest one is yours
console.log("account", account.name, "ledger id", account.id);
if (Number(dollars) > 0) {
console.log("deposit:", await act(call("deposit", account.id, usd(Number(dollars), true))));
await until(read, (st) => Object.values(st.accounts).find((a) => a.id === account.id).depositUSD >= Number(dollars) - 0.01);
console.log("funded");
}WALLET_KEY=0x… node setup.mjs trade 100 # an OPTIONS account with $100 behind it
WALLET_KEY=0x… node setup.mjs protect 100 # a PROTECT account
WALLET_KEY=0x… node setup.mjs earn 450 # an EARN (maker) accountOpen an OPTIONS account on the Temporal testnet venue and put $100 of my free cash behind it, using setup.mjs from https://protect.temporal.exchange/docs/wallet-and-accounts. Read my state first and tell me my cash. After it is done, read my state again and show me the account's name, ledger id and deposit.
Check the result yourself: the money-trail check.
There is no house#
The venue owns nothing and quotes nothing. The first curve on the book is the first one a person funds and posts; until then nothing trades. Every quote is backed by an account's own deposit.
Four account kinds#
The kind is fixed when the account is opened. OPTIONS holds the options you open from the OPTION ticket, with no perp behind them (accounts opened before the rename, such as TRADE 1, keep their name and wear the OPTIONS badge). PROTECT holds exactly one perp and one option pair. EARN holds one quoted curve and the positions it accrues against traders; a maker holds no perp of its own.
A name is the kind and a number — OPTIONS 1, PROTECT 1, EARN 1 — counting every account of that kind the wallet has opened. It is given once and never changes. An account is active until CLOSE ACCOUNT ends it; an ended account is inactive and sits behind SHOW INACTIVE.
Margin: one closed form, summed at the account#
Every sold option is margined by the traditional closed form, with no scenario and no path length. Per unit of coin written: the initial margin is the larger of (15 % − how far out of the money the strike is) and 10 %, times spot, plus the option's mark. The maintenance margin is 7.5 % of spot plus the mark. An in-the-money option has zero distance out of the money, so it takes the full 15 %, and its mark is never below its intrinsic.
The legs add up at the account. An open must fit the initial margin; liquidation starts when equity is at or below the maintenance margin. Bought options count as collateral at their mark, and a sold vertical is margined at its width. Makers and takers use the same formula: an EARN account's sold rows are margined at the maker's own mark, exactly as an OPTIONS account's are.
The covered wing is exempt in PROTECT: the perp covers the sold option up to its quantity (a long perp covers calls, a short perp puts), and only the excess is margined. Perps keep their own margin at the leverage you choose. The contract runs this formula on chain; the keeper pushes the marks it reads.
What happens when equity reaches the maintenance margin — the close, the fee, the insurance fund and why a later deposit is never charged for an old loss — is in section 6.
PROTECT: buy a floor, solved for you#
Choose PROTECT on CREATE PERP and the perp opens straight into its own PROTECT account, on a long or a short. You type three numbers, all pre-filled: a FLOOR in dollars, a TARGET ROE as the return on equity you are playing for, and PROTECTION LASTS in days (30 unless you change it). The venue buys a put near the floor and sells a call at or beyond the target (the other way round on a short), picking each strike and decay itself.
The solver is one fixed problem. It looks for the structure that beats a less-levered perp with the same liquidation floor by the most, in points of your return at the target, and still lasts at least the days you asked (30 unless you change it) (the ticket's headline, for example "Keeps 72% of your upside · liquidation −18% → −30% · protection holds 30 days") subject to these rules: the liquidation floor lands at the one you typed, or at most one point of the price further away, never nearer; the two legs are worth the same at their fill prices, so no premium changes hands; neither leg is larger than the perp; every leg is something the makers will actually quote right now; and the protection lasts as many days as you asked, so the very short, bleed-heavy puts are out: if the price falls to where the bare perp would have been liquidated and stays there, the account's cushion covers the structure's net running cost for that long. Among near-equal answers it takes the floor closest to yours, then the lowest peak funding. If no structure lasts the default minimum, nothing opens and the refusal names the longest one that does; if you typed the days, you get the longest one and a line saying so. If none can hold the floor, nothing opens, and the refusal names the nearest floor that can be held. The floor you type stays as you typed it, and one line under it says where the structure really liquidates.
The perp and its options open as one request. You sign once; the venue fills the perp on HyperCore and the contract opens every option leg in the same transaction inside the price limit you signed. If any part fails, everything is cancelled and your margin goes back: you are never left with a perp and no protection, or protection and no perp. The ticket shows FUNDING / YR, what the two legs cost or earn over a year, with the rate now and the worst rate near the floor. The PAYOFF tab draws the structure against a plain perp on the same floor (section 8).
Do this from your agent
PROTECT: a perp with its floor, in one signed request
Ask the venue for the structure that puts your floor where you typed it. side is 1 for a long and -1 for a short; target is the floor in dollars. The plan reads the book and changes nothing. A floor the venue cannot deliver is refused with the nearest one it can.
curl -s -X POST https://protect.temporal.exchange/api/venue/perp/plan -H 'content-type: application/json' \
-d '{"side":1,"depositUSD":100,"lev":3,"asset":"BTC","target":52000,"sellFromRoe":1,"mode":"protect"}'{"ok":true,"plan":{"ok":true,"floorUSD":52000,"markAt":79702.6,"feeTotalUSD":0.25,
"legs":[
{"role":"bought","wing":"put","K":52000,"rho":2.4,"qty":0.001447,"premiumUSD":1.29,"feeUSD":0.035},
{"role":"sold","wing":"call","K":112934.68,"rho":1.0909,"qty":0.000367,"premiumUSD":1.29,"feeUSD":0.009}], ...}}{"ok":true,"plan":{"ok":false,"reason":"The nearest floor this structure delivers is $53,807, further from the price than the $60,000 you typed - type $53,807 or a floor beyond it."}}Then the script quotes each leg, bounds it (a bought leg may cost at most 1 % over its quote, a sold leg may pay at most 1 % under), checks the whole request with a dry run, and signs openPerpWithLegs once. The venue's fill opens the perp and every leg together inside those bounds, or cancels the whole request and your margin comes back.
// protect.mjs <account> <long|short> <marginUSD> <leverage> <floorUSD> [BTC|HYPE]: open a perp WITH its protection in ONE signed request.
// <account> is a PROTECT account (setup.mjs protect <dollars>). The venue's fill opens the perp and every leg together inside your bounds, or cancels the whole request and your margin comes back.
import { wallet, get, post, call, check, act, usd, coin, strike, decay, ASSET } from "./temporal.mjs";
const [account, dir, marginUSD, leverage, floorUSD, asset = "BTC"] = process.argv.slice(2);
const long = dir === "long";
// 1. The venue's plan for that floor: the legs it would buy and sell. Reads the book, changes nothing. A refusal names the nearest floor it can deliver.
const { plan } = await post("/perp/plan", { side: long ? 1 : -1, depositUSD: Number(marginUSD), lev: Number(leverage), asset, target: Number(floorUSD), sellFromRoe: 1, mode: "protect" });
if (!plan.ok) throw new Error(plan.reason);
const mark = plan.markAt;
const perpQty = Math.floor(((Number(marginUSD) * Number(leverage)) / mark) * 1e6) / 1e6; // the perp's size, to the ledger's unit (6 decimals of a coin)
// 2. Quote every leg at its planned size and bound it: a bought leg may cost at most 1 % over its quote, a sold leg may pay at most 1 % under. SOLD legs go first.
const legs = [];
for (const l of [...plan.legs].sort((a, b) => (a.role === "sold" ? 0 : 1) - (b.role === "sold" ? 0 : 1))) {
const months = 12 / l.rho;
const q = await get(`/chain/quote?asset=${asset}&side=${l.role === "sold" ? "sell" : "buy"}&wing=${l.wing}&strikeUSD=${l.K}&qty=${l.qty}&months=${months}&wallet=${wallet.address}`);
if (!q.ok) throw new Error(`no quote for the ${l.wing}: ${JSON.stringify(q)}`);
const bound = l.role === "sold" ? q.pricePerUnitUSD * 0.99 : q.pricePerUnitUSD * 1.01;
const consentUSD = (l.role === "bought" ? l.qty * bound : 0) + (legs.length === 0 ? plan.feeTotalUSD + 1 : 0); // a ceiling on what the leg may pull; the ledger takes only what is short
legs.push([l.wing === "call" ? 0 : 1, l.role === "sold" ? 1 : 0, strike(l.K), coin(l.qty), decay(months), usd(bound, l.role === "bought"), usd(consentUSD, true)]);
}
const data = call("openPerpWithLegs", account, ASSET[asset], long ? 0 : 1, coin(perpQty), Number(leverage), usd(Number(marginUSD), true), legs);
const dry = await check(data);
if (!dry.ok) throw new Error(`the venue would refuse: ${dry.reason} (${dry.errorName ?? dry.code})`);
if (process.env.CONFIRM !== "yes") process.exit(console.log(`Dry run passed: ${perpQty} ${asset} ${dir}, floor ${plan.floorUSD}. Run again with CONFIRM=yes to sign and send.`));
console.log(await act(data));
// Then watch /chain/state: the account's perps[] shows "requested", then "open" with its legs. A cancelled request shows no open perp and the margin back in free cash.WALLET_KEY=0x… node protect.mjs 5 long 100 3 52000 BTC # dry run: shows the plan, signs nothing
CONFIRM=yes WALLET_KEY=0x… node protect.mjs 5 long 100 3 52000 BTC # sign and sendOn the Temporal testnet venue, open a PROTECT: long BTC, $100 margin, 3x leverage, floor $52,000, on my PROTECT account. Use the plan route and protect.mjs from https://protect.temporal.exchange/docs/wallet-and-accounts. Show me the plan (legs, strikes, fee, the floor it really delivers) and the dry-run result, and wait for my yes before sending. After it is sent, watch /chain/state until the perp shows open and tell me the floor and the account's margin.
A plain openPerp on a PROTECT account is refused by the contract: the only door is openPerpWithLegs. A PROTECT account closes only as a whole (see Leaving a position).
Check the result yourself: the PROTECT floor check.
PORTFOLIO#
PORTFOLIO lists every account with its figures in the asset's own coin; the one place that sums across assets, the wallet total, is in dollars. The perp rows carry size, entry, mark, P/L and perp equity. The option rows carry strike (in dollars, at entry as a % from the mark, and now), entry price, current price, P/L and equity. Closed legs are kept behind SHOW CLOSED POSITIONS.
FUNDING is its own figure: the wallet-wide sum paid (−) or received (+) on option legs, with each leg's yearly run rate in its hover. The strip also shows Perps P/L, Options P/L, Earn P/L, net perp exposure, the positions' equity and the MAINTENANCE LEVEL — the equity at which the account liquidates.
Money that reaches the venue with no act behind it (a transfer sent straight to the venue's wallet) is never shown as a balance. A banner offers two actions: MOVE INTO AN ACCOUNT, or SEND BACK to your HyperCore wallet. On a chain wallet, accounts that exist only in the venue's old test history are legacy (pre-ledger) accounts: they are counted in one muted line, not in the totals, because the ledger is the account of record.
Assets#
The venue trades BTC and HYPE. Each asset is its own book with its own marks, makers and positions, and a maker's curve quotes exactly one of them. Marks come from Hyperliquid. Two coins have no common unit, so anything summed across assets is in dollars.
Signing in#
Connect your own wallet; the venue asks for a signature and your accounts live under that address. This deployment is a TESTNET venue: no real money is at risk, so it also offers a throwaway testnet wallet, a key generated in your browser. You sign an intent for each act; the venue's own signer submits it and pays the gas, so a wallet never needs to hold the chain's gas coin. On testnet a wallet's own accounts may fill each other, which lets one person hold both sides of a test. Mainnet runs the same venue on real funds — see section 9.
Do this from your agent
Signing in from an agent
Acts and reads need no sign-in at all: the signature on each act is the authority. Only a maker signs in, to start quoting and post its curve (Earn (making)). That sign-in is a signed message that moves no money and costs no gas.
// session.mjs: sign in to the venue as a MAKER. The signature proves you hold the wallet; it moves no money and costs no gas.
// Returns the bearer token the maker routes (/account/quoting, /account/curve, /account/curve/raw) need.
import { wallet, post } from "./temporal.mjs";
export async function signIn() {
const { message } = await post("/auth/nonce", { address: wallet.address });
const signature = await wallet.signMessage(message); // EIP-191 personal_sign, the wallet's own signMessage
const { token } = await post("/auth/verify", { address: wallet.address, signature });
return { Authorization: `Bearer ${token}` };
}Check the result yourself: all the checks.
Kept in lockstep with the code: a check fails if this page drifts.