Skip to main content

Prerequisites

  • Sepolia ETH for gas fees
  • MockUSDC in your wallet (see Quick Start to mint test tokens)
  • A Sepolia RPC URL

Parameters

Every position requires four parameters:
ParameterTypeDescription
SideLONG (0) or SHORT (1)LONG profits when EUR/USD rises, SHORT profits when it falls
Tenor1D (0), 1W (1), or 1M (2)Time to maturity — determines fixing date and initial margin requirement
NotionalUSDC amount (6 decimals)Position size — the face value of the forward contract
MarginUSDC amount (6 decimals)Collateral locked for the position — must meet the initial margin requirement
The initial margin requirement is a percentage of notional (configured per-tenor). For example, if the IM factor is 500 bps (5%) and your notional is 10,000 USDC, you need at least 500 USDC in margin.

Steps

1

Check protocol mode

The protocol must be in NORMAL mode to open new positions. Other modes (DEGRADED, REDUCE_ONLY, PAUSED) only allow closing or are fully halted.
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { modeControllerAbi, Mode } from "@nile-markets/sdk";

const client = createPublicClient({
  chain: sepolia,
  transport: http("YOUR_RPC_URL"),
});

const MODE_CONTROLLER = "0x..."; // See Contract Addresses page

const mode = await client.readContract({
  address: MODE_CONTROLLER,
  abi: modeControllerAbi,
  functionName: "currentMode",
});

if (mode !== Mode.NORMAL) {
  throw new Error(`Cannot open positions — protocol mode is ${mode}`);
}
2

Approve USDC spending

The PositionManager needs approval to pull USDC from your wallet when opening a position. Approve at least the margin amount you plan to lock.
import { createWalletClient, http, parseUnits } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { mockUsdcAbi } from "@nile-markets/sdk";

const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const walletClient = createWalletClient({
  account,
  chain: sepolia,
  transport: http("YOUR_RPC_URL"),
});

const USDC = "0x...";              // See Contract Addresses page
const POSITION_MANAGER = "0x...";  // See Contract Addresses page

const margin = parseUnits("500", 6); // 500 USDC

const approveHash = await walletClient.writeContract({
  address: USDC,
  abi: mockUsdcAbi,
  functionName: "approve",
  args: [POSITION_MANAGER, margin],
});

await client.waitForTransactionReceipt({ hash: approveHash });
console.log("USDC approved");
3

Simulate the position

Run a dry-run to verify the trade will succeed and see the computed entry strike, required margin, and fees before committing real funds.
import { positionManagerAbi, Side, Tenor, PAIR_IDS } from "@nile-markets/sdk";
import { parseUnits, formatUnits } from "viem";

const POSITION_MANAGER = "0x..."; // See Contract Addresses page

const notional = parseUnits("10000", 6);
const margin = parseUnits("500", 6);

// Simulate via eth_call (no gas spent)
const result = await client.simulateContract({
  address: POSITION_MANAGER,
  abi: positionManagerAbi,
  functionName: "openPosition",
  args: [PAIR_IDS.EUR_USD, Side.LONG, notional, Tenor.ONE_WEEK, margin],
  account: account.address,
});

console.log(`Position ID: ${result.result}`);
If the margin is insufficient, the simulation reverts with InsufficientMargin — increase your margin amount and retry.
Simulation uses eth_call and does not consume gas or change onchain state. The entry strike shown is based on the current forward price and may differ slightly by the time you submit the real transaction.
4

Open the position

Submit the actual transaction to open your forward position.
import { positionManagerAbi, Side, Tenor, PAIR_IDS } from "@nile-markets/sdk";
import { parseUnits } from "viem";

const POSITION_MANAGER = "0x..."; // See Contract Addresses page

const notional = parseUnits("10000", 6); // 10,000 USDC
const margin = parseUnits("500", 6);     // 500 USDC

const hash = await walletClient.writeContract({
  address: POSITION_MANAGER,
  abi: positionManagerAbi,
  functionName: "openPosition",
  args: [PAIR_IDS.EUR_USD, Side.LONG, notional, Tenor.ONE_WEEK, margin],
});

const receipt = await client.waitForTransactionReceipt({ hash });
console.log(`Position opened: ${receipt.transactionHash}`);
Parse the PositionOpened event from the receipt logs to extract the position ID:
import { decodeEventLog } from "viem";

for (const log of receipt.logs) {
  try {
    const event = decodeEventLog({
      abi: positionManagerAbi,
      data: log.data,
      topics: log.topics,
    });
    if (event.eventName === "PositionOpened") {
      console.log(`Position ID: ${event.args.positionId}`);
      console.log(`Entry strike: ${formatUnits(event.args.entryStrike, 18)}`);
    }
  } catch {
    // Skip non-matching logs
  }
}
5

Verify the position

Confirm your position is open and review its parameters.
import { positionManagerAbi } from "@nile-markets/sdk";
import { formatUnits } from "viem";

const POSITION_MANAGER = "0x..."; // See Contract Addresses page
const positionId = 1n; // From the PositionOpened event

const position = await client.readContract({
  address: POSITION_MANAGER,
  abi: positionManagerAbi,
  functionName: "getPosition",
  args: [positionId],
});

console.log(`Side: ${position.side === 0 ? "LONG" : "SHORT"}`);
console.log(`Notional: ${formatUnits(position.notional, 6)} USDC`);
console.log(`Entry strike: ${formatUnits(position.entryStrike, 18)}`);
console.log(`Margin locked: ${formatUnits(position.imLocked, 6)} USDC`);
console.log(`Fixing: ${new Date(Number(position.fixingTimestamp) * 1000).toISOString()}`);

What Happens Next

After opening, your position accrues unrealized PnL based on the current forward price relative to your entry strike. Three outcomes are possible:
  1. Maturity settlement — the position reaches its fixing date and settles at the fixing price
  2. Early termination — you close early at the current forward price (see Close a position)
  3. Liquidation — your equity drops below the maintenance margin threshold and a keeper liquidates the position
Monitor your positions in real time using the Monitor Positions guide.

Troubleshooting

Your margin amount is below the initial margin requirement. Run the simulation first to see the exact IM needed, then increase your margin parameter. The IM factor is a percentage of notional that varies by tenor — longer tenors require more margin.
You did not approve enough USDC for the PositionManager. Run the approve step again with at least the margin amount. The allowance must cover the full margin plus any fees deducted at open.
The protocol is not in NORMAL mode. Check the mode (Step 1) and wait until it returns to NORMAL. Only NORMAL mode allows opening new positions.
The pool’s exposure limits prevent this position. The RiskManager enforces per-position, per-account, and pool-level caps. Try a smaller notional or wait for existing positions to settle.