> ## Documentation Index
> Fetch the complete documentation index at: https://docs.outcome.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Place HIP-4 orders, cancel, and convert outcome tokens

> PredictionTradingAdapter API: place limit and market orders on HIP-4 outcomes, cancel resting orders, and run split, merge, and negate conversions.

The Trading Adapter (`adapter.trading`) lets you submit, modify, and cancel orders on HIP-4 prediction market outcomes. Before you can place any orders you must authenticate using `adapter.auth.initAuth()`. Without a signer, the order methods return an error result and `cancelOrder` throws. Limit orders go through local checks (price formatting, minimum notional, and minimum shares) before signing, so many errors are caught before they reach the exchange.

<Warning>
  You must call `adapter.auth.initAuth(walletAddress, signer)` before using any
  trading methods. See the [Auth Adapter](/sdk/reference/auth-adapter) page for
  the full authentication flow, including agent key setup.
</Warning>

***

## Configuration

### `minOrderNotional`

Client-side order-notional floor in USD, checked before submission. Optional - defaults to the
SDK's own `MIN_NOTIONAL` (the real Hyperliquid protocol minimum, currently \$1). Set it on
`createHIP4Adapter` to apply a stricter, business-chosen floor on top of the protocol minimum:

```typescript theme={null}
const adapter = createHIP4Adapter({
  testnet: false,
  minOrderNotional: 10, // reject orders below $10 notional, even though the protocol allows $1
});
```

`minOrderNotional` can only raise the effective floor, never lower it - the adapter constructor
throws if you pass a value below `MIN_NOTIONAL`. Once set, it applies to both of `placeOrder`'s
local checks: the notional check and, when `markPx` is provided, the minimum
shares check (`getMinShares(markPx, minOrderNotional)`). Pass `skipMinNotionalCheck: true` on an
individual `placeOrder` call to skip these SDK-side pre-checks - this does **not** change anything
Hyperliquid itself enforces; it only matters for cases like closing an existing position, where the
residual size may be below the configured minimum but the exchange still accepts the order. See
[`skipMinNotionalCheck`](#param-skip-min-notional-check) below, and
[`MIN_NOTIONAL`](/sdk/reference/utilities#min_notional) in the Utilities reference for the
underlying constant.

<Note>
  `minOrderNotional` is set once, at adapter construction - there is no per-order override. Use
  `skipMinNotionalCheck` on an individual `placeOrder` call if a specific order needs to bypass it.
</Note>

***

## `placeOrder(params)`

Places a single order on a HIP-4 outcome. Validation failures and exchange rejections come back in the result, so check the `success` field and the `error` message instead of catching. The method throws only for an unsupported `timeInForce` (`"FOK"` or `"GTD"`) or an `outcome` coin whose side index isn't 0 or 1.

For limit orders, the SDK performs these local checks before signing:

* Rounds the price to the outcome tick with [`formatOutcomePrice`](/sdk/reference/utilities#formatoutcomeprice-price): at most 5 significant figures and at most 5 decimals, so `"0.012345"` becomes `"0.01235"`.
* Checks that the notional value meets the minimum: \$1 (`MIN_NOTIONAL`), or the adapter's [`minOrderNotional`](#minordernotional) if you set a higher one. Notional is `price × amount`, or `amount × max(0.01, min(markPx, 1 - markPx))` when you pass `markPx`.
* When `markPx` is provided, checks that `amount` is at least `getMinShares(markPx)`.

`skipMinNotionalCheck: true` skips both checks.

For market orders, the SDK sends price `"0.99999"` for buys and `"0.00001"` for sells with Hyperliquid's `FrontendMarket` time-in-force, so the exchange fills at the best available prices. The SDK doesn't run the notional checks on market orders.

```typescript theme={null}
const result = await adapter.trading.placeOrder({
  marketId: "516",
  outcome: "#5160",
  side: "buy",
  type: "limit",
  price: "0.65",
  amount: "100",
});

if (!result.success) {
  console.error(result.error);
}
```

### Parameters

<ParamField path="marketId" type="string" required>
  The outcome ID as a string (e.g. `"516"`).
</ParamField>

<ParamField path="outcome" type="string" required>
  The side identifier. Use the coin string (e.g. `"#5160"` for side 0, `"#5161"`
  for side 1). This determines which side of the outcome you are trading.
</ParamField>

<ParamField path="side" type="string" required>
  `"buy"` or `"sell"`.
</ParamField>

<ParamField path="type" type="string" required>
  `"market"` or `"limit"`.
</ParamField>

<ParamField path="price" type="string">
  Limit price as a decimal string (0 to 1). Required for limit orders. Ignored for
  market orders, which use `"0.99999"` (buy) or `"0.00001"` (sell).
</ParamField>

<ParamField path="amount" type="string" required>
  Order size in shares.
</ParamField>

<ParamField path="timeInForce" type="string" default="GTC">
  Time-in-force for limit orders. One of `"GTC"`, `"ALO"`, `"FAK"`, `"FOK"`,
  `"GTD"`. `"FOK"` and `"GTD"` aren't supported and throw. See the TIF mapping
  table below. Ignored for market orders.
</ParamField>

<ParamField path="markPx" type="number">
  Current market price (0 to 1). When provided, the SDK enforces a minimum shares check, `amount >= getMinShares(markPx)`, and measures notional on the cheaper side's price. This ensures the order meets the \$1 minimum notional (or your `minOrderNotional`).
</ParamField>

<ParamField path="builderAddress" type="string">
  Optional builder address that receives builder fees. Checksummed addresses
  are accepted; the SDK lowercases the value before signing. Overrides the
  adapter-level `builderAddress`.
</ParamField>

<ParamField path="builderFee" type="number">
  Builder fee in tenths of a basis point. `0` = no fee. `100` = 0.1%. Maximum is
  `1000` (1.0%). Ignored when no builder address is set on the order or the
  adapter.
</ParamField>

<ParamField path="skipMinNotionalCheck" type="boolean">
  When `true`, skips the SDK's local minimum-notional and minimum-shares
  pre-checks. Use this for position-closing flows where the residual size may be
  below \$1 but the exchange still accepts the order.
</ParamField>

### TIF mapping

The SDK maps SDK-level TIF values to Hyperliquid order types as follows:

| SDK `timeInForce` | Order `type` | Hyperliquid TIF | Notes |
| - | - | - | - |
| `"GTC"` (default) | `"limit"` | `Gtc` | Good-till-cancelled |
| `"ALO"` | `"limit"` | `Alo` | Add liquidity only (post-only) |
| `"FAK"` | `"limit"` | `Ioc` | Immediate-or-cancel |
| `"FOK"` | `"limit"` | None | Not supported; `placeOrder` throws |
| `"GTD"` | `"limit"` | None | Not supported; `placeOrder` throws |
| *(any)* | `"market"` | `FrontendMarket` | Price `0.99999` (buy) or `0.00001` (sell) |

### Return type

`Promise<PredictionOrderResult>`

<ResponseField name="success" type="boolean" required>
  `true` if the order was accepted by the exchange.
</ResponseField>

<ResponseField name="orderId" type="string">
  Hyperliquid order ID. Present when the order filled or is resting.
</ResponseField>

<ResponseField name="status" type="string">
  `"filled"` - fully executed. `"resting"` - sitting in the book. `"error"` -
  exchange rejected the order. `"unknown"` - unrecognized response.
</ResponseField>

<ResponseField name="shares" type="string">
  Filled size. Only present when `status === "filled"`.
</ResponseField>

<ResponseField name="error" type="string">
  Error message. Present when `success === false`. When Hyperliquid rejects the
  whole request, this is `Exchange returned non-ok status`.
</ResponseField>

<ResponseField name="raw" type="string">
  Hyperliquid's own message when it rejects the whole request, for example
  `User or API Wallet 0x... does not exist.` when the agent isn't approved. Set
  only when Hyperliquid sends one. Available from 1.3.0.
</ResponseField>

### Examples

```typescript theme={null}
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import {
  createHIP4Adapter,
  getAgentApprovalTypedData,
  submitAgentApproval,
} from "@outcome.xyz/hip4";

// userAccount: the user's viem account; walletClient: a viem WalletClient for it
const adapter = createHIP4Adapter({ testnet: false });
await adapter.initialize();

// --- Authentication (agent key flow) ---
const agent = privateKeyToAccount(generatePrivateKey());
const nonce = Date.now(); // sign and submit with the same nonce
const typedData = getAgentApprovalTypedData(
  agent.address,
  "My App",
  nonce,
  true, // true = mainnet, matching testnet: false above
);
const sig = await walletClient.signTypedData({
  account: userAccount,
  domain: typedData.domain,
  types: typedData.types,
  primaryType: typedData.primaryType,
  message: typedData.message,
});
const approval = await submitAgentApproval(sig, agent.address, "My App", nonce, true);
if (!approval.success) {
  throw new Error(`Agent approval failed: ${approval.error}`);
}
await adapter.auth.initAuth(userAccount.address, agent);

// --- Place a limit buy order ---
const limitResult = await adapter.trading.placeOrder({
  marketId: "516",
  outcome: "#5160", // side 0 (e.g. "Yes")
  side: "buy",
  type: "limit",
  price: "0.65",
  amount: "100",
  timeInForce: "GTC",
  markPx: 0.65, // enables minimum-shares validation
});

if (limitResult.success) {
  console.log("Order status:", limitResult.status);
  // "filled" or "resting"
  if (limitResult.status === "filled") {
    console.log("Filled shares:", limitResult.shares);
  }
} else {
  console.error("Order failed:", limitResult.error);
}

// --- Place a market sell order ---
const marketResult = await adapter.trading.placeOrder({
  marketId: "516",
  outcome: "#5161", // side 1 (e.g. "No")
  side: "sell",
  type: "market",
  amount: "50",
});

// --- Limit buy with builder fee ---
const builderResult = await adapter.trading.placeOrder({
  marketId: "516",
  outcome: "#5160",
  side: "buy",
  type: "limit",
  price: "0.70",
  amount: "25",
  builderAddress: "0xYourBuilderAddress",
  builderFee: 100, // 0.1%
});

// --- Close a small residual position, skipping notional check ---
const closeResult = await adapter.trading.placeOrder({
  marketId: "516",
  outcome: "#5160",
  side: "sell",
  type: "limit",
  price: "0.60",
  amount: "1", // $0.60 notional, below the $1 minimum
  skipMinNotionalCheck: true,
});
```

***

## `placeOrders(params[])`

Places several orders in one signed request. Each order goes through the same local checks as `placeOrder`. Orders that fail them get an error result and aren't sent; the rest go to the exchange together.

```typescript theme={null}
const batch = await adapter.trading.placeOrders([
  { marketId: "516", outcome: "#5160", side: "buy", type: "limit", price: "0.60", amount: "10" },
  { marketId: "516", outcome: "#5160", side: "buy", type: "limit", price: "0.58", amount: "10" },
]);

batch.success; // true only when every order succeeded
batch.results; // one PredictionOrderResult per input, in input order
```

Returns `Promise<PredictionBatchOrderResult>`. An empty array returns `{ success: true, results: [] }`. When Hyperliquid rejects the whole request, every sent order's result has `success: false`, the `error` `Exchange returned non-ok status`, and Hyperliquid's message in `raw`.

<Note>
  `placeOrders` always uses the adapter-level `builderAddress` and `builderFee`.
  Per-order builder fields are ignored in a batch.
</Note>

***

## `modifyOrder(params)`

Changes the price or size of a resting limit order. Hyperliquid keeps the order's queue priority when only the size changes; a price change moves it to the back of the queue at the new level.

```typescript theme={null}
const result = await adapter.trading.modifyOrder({
  marketId: "516",
  outcome: "#5160",
  orderId: "12345",
  side: "buy",
  type: "limit",
  price: "0.62",
  amount: "100",
});
```

Pass the `orderId` plus the order's `marketId`, `outcome`, and `side`, the new `price` and `amount`, and optionally `timeInForce` and `markPx`. `type` must be `"limit"`. The same local checks as `placeOrder` run, and you can't skip the notional check because `skipMinNotionalCheck` isn't a modify parameter. Returns `Promise<PredictionOrderResult>`, with the (possibly new) order ID in `orderId`.

***

## `cancelOrder(params[])`

Cancels one or more resting orders in a single request.

```typescript theme={null}
await adapter.trading.cancelOrder([
  { marketId: "516", orderId: "12345", outcome: "#5160" },
]);
```

### Parameters

`cancelOrder` accepts an **array** of cancel requests:

<ParamField path="[].marketId" type="string" required>
  The outcome ID as a string.
</ParamField>

<ParamField path="[].orderId" type="string" required>
  The Hyperliquid order ID to cancel. Obtain this from `placeOrder`'s `orderId`
  field or from `adapter.account.fetchOpenOrders()`.
</ParamField>

<ParamField path="[].outcome" type="string">
  Optional side identifier (e.g. `"#5160"`). Providing this resolves the correct
  side asset ID. When omitted, the SDK defaults to side 0.
</ParamField>

### Return type

`Promise<HLCancelResponse>`: Hyperliquid's cancel response. `status` is `"ok"` or `"err"`, and `response.data.statuses` holds one entry per cancel: `"success"` or `{ error: string }`.

`cancelOrder` throws when you aren't authenticated, when you pass an empty array, or when the request itself fails (for example a network or HTTP error). Exchange-level rejections don't throw, so check the returned statuses.

<Warning>
  The SDK resolves the cancel asset ID to **side 0** unless you provide the
  `outcome` field. If you placed an order on side 1 (`"#5161"`), you must pass
  `outcome: "#5161"` to cancel it correctly.
</Warning>

### Example

```typescript theme={null}
import { parseSideCoin } from "@outcome.xyz/hip4";

// Cancel a single resting order
const res = await adapter.trading.cancelOrder([
  { marketId: "516", orderId: "12345", outcome: "#5160" },
]);
console.log(res.response?.data.statuses); // e.g. ["success"]

// Cancel multiple orders at once
await adapter.trading.cancelOrder([
  { marketId: "516", orderId: "12345", outcome: "#5160" },
  { marketId: "516", orderId: "67890", outcome: "#5161" },
]);

// Fetch open orders first, then cancel the HIP-4 ones
const openOrders = await adapter.account.fetchOpenOrders(userAddress);
const cancels = openOrders.flatMap((order) => {
  const parsed = parseSideCoin(order.coin); // null for non-HIP-4 coins
  return parsed
    ? [{ marketId: String(parsed.outcomeId), orderId: String(order.oid), outcome: order.coin }]
    : [];
});
if (cancels.length > 0) {
  await adapter.trading.cancelOrder(cancels);
}
```

***

## Token conversions

Four protocol-level share-conversion primitives that move value between USDC and outcome tokens without touching the orderbook. They settle directly against your spot balances at the bundle-equivalence mint price - no spread paid, no slippage, no liquidity required. See [Splitting and merging](/outcome-tokens-and-pricing#splitting-and-merging) for the concept, and the [Token conversions guide](/sdk/guides/conversions) for end-to-end code.

All four methods use the same `userOutcome` action envelope, sign via L1 agent signing, and return a `WalletActionResult`. None of them throw.

```typescript theme={null}
type WalletActionResult = {
  success: boolean;
  error?: string;
  filledSz?: string;
  avgPx?: string;
  oid?: number;
};
```

<Warning>
  Conversions affect your spot balances immediately on `success: true`. The SDK
  does not preview or simulate. Validate inputs before calling.
</Warning>

***

## `splitOutcome(params)`

Burn `X` USDC and mint `X` Yes shares + `X` No shares of one outcome.

```typescript theme={null}
await adapter.trading.splitOutcome({
  outcome: 516,
  amount: "100",
});
```

### Parameters

<ParamField path="outcome" type="number" required>
  Numeric outcome ID. Matches `market.outcomeId` on a fetched HIP-4 market.
</ParamField>

<ParamField path="amount" type="string" required>
  USDC amount to split, as a decimal string (e.g. `"12.5"`). The SDK strips
  trailing zeros to match Hyperliquid's wire format. Burning `X` USDC mints `X`
  Yes and `X` No.
</ParamField>

### Return type

`Promise<WalletActionResult>` - never throws.

***

## `mergeOutcome(params)`

The inverse of `splitOutcome`. Burn `X` Yes + `X` No of one outcome and mint `X` USDC.

```typescript theme={null}
await adapter.trading.mergeOutcome({
  outcome: 516,
  amount: "100",
});

// Or burn the max available:
await adapter.trading.mergeOutcome({ outcome: 516, amount: null });
```

### Parameters

<ParamField path="outcome" type="number" required>
  Numeric outcome ID.
</ParamField>

<ParamField path="amount" type="string | null" required>
  Paired-share count to merge, as a decimal string. Pass `null` to merge the
  maximum available - the protocol burns `min(yes_balance, no_balance)` shares.
</ParamField>

### Return type

`Promise<WalletActionResult>` - never throws.

***

## `mergeQuestion(params)`

Burn `X` Yes shares from every outcome of a question (including the fallback) and mint `X` USDC. Lets you redeem a full Yes-bundle for collateral before the question resolves.

```typescript theme={null}
await adapter.trading.mergeQuestion({
  question: 42,
  amount: "10",
});

// Or redeem the max available:
await adapter.trading.mergeQuestion({ question: 42, amount: null });
```

### Parameters

<ParamField path="question" type="number" required>
  Numeric question ID. For multi-outcome and price-bucket markets, this is
  `market.questionId`. Default-binary markets have no parent question - use
  `mergeOutcome` instead.
</ParamField>

<ParamField path="amount" type="string | null" required>
  Yes-share count to redeem from each member outcome, as a decimal string. Pass
  `null` to redeem the maximum - `min(yes_balance)` across every outcome of the
  question.
</ParamField>

### Return type

`Promise<WalletActionResult>` - never throws.

***

## `negateOutcome(params)`

Burn `X` No shares of one outcome and mint `X` Yes shares of every **other** outcome in the same question (including the fallback). Converts "I don't think this wins" into "I think one of the others wins" without touching the orderbook.

```typescript theme={null}
await adapter.trading.negateOutcome({
  question: 42,
  outcome: 516,
  amount: "5",
});
```

### Parameters

<ParamField path="question" type="number" required>
  Numeric question ID containing the source outcome.
</ParamField>

<ParamField path="outcome" type="number" required>
  Source outcome ID whose No shares are being converted. Must belong to
  `question`.
</ParamField>

<ParamField path="amount" type="string" required>
  No-share count to convert, as a decimal string. After the call, your No
  balance on `outcome` drops by `amount`, and you hold `amount` additional Yes
  shares of every other outcome under `question`.
</ParamField>

### Return type

`Promise<WalletActionResult>` - never throws.

<Note>
  The on-wire sub-action key is `negateOutcome`, matching Hyperliquid's testnet
  "Convert Outcomes" UI. Hyperliquid's docs body shows `negateQuestion` in
  places - that's a typo in their docs. The SDK sends `negateOutcome`.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.