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

# Trade perps with Yield.xyz

> Learn how to fund a trading account, open and manage leveraged perpetual-futures positions, and sign and submit the resulting actions with the Web SDK and the Yield.xyz integration.

Portal's Web SDK lets your users trade perpetual futures on venues such as Hyperliquid through the `portal.perps.yieldXyz` API. This guide covers discovering venues and markets, funding a trading account, opening and managing leveraged positions, and signing, submitting and tracking each action with the Portal wallet.

## Overview

With `portal.perps.yieldXyz` you can:

* **Discover** the trading venues (providers) and their markets, with mark and oracle prices, funding rates, leverage ranges, fees and price candles
* **Fund** a trading account and complete any one-time venue approvals
* **Open**, **manage** and **close** leveraged long or short positions, and **withdraw** collateral
* **Execute** an action in one call: the SDK signs each transaction with the Portal wallet in the format it requires (EIP-712 typed data or an on-chain transaction), submits it to Yield.xyz and tracks the action to a final status
* **Track** positions, orders, account balances, past actions and venue events

The [Yield.xyz Perps integration page](/integrations/Yield/yield-xyz-perps#how-perpetual-futures-trading-works) explains how perpetual-futures trading works. Every method except `executeAction` and `signAndSubmitAction` resolves with the Portal API response, wrapped as `{ data }`; those two resolve with the execution result itself. Venues are identified by an opaque `providerId` (for example `hyperliquid`); real chains, where they appear, use [CAIP-2](/resources/chain-id-formatting) (`eip155:42161` for Arbitrum One).

<Note>
  This guide uses **Hyperliquid**, the launch venue, as the example. A venue's supported actions and the `args` each action takes come from `getProviders` (`supportedActions` and `argumentSchemas`), and how each transaction is signed follows the `signingFormat` Yield.xyz returns on it, not the venue.
</Note>

## Prerequisites

Before using perps operations, ensure you have:

* A properly initialized Portal client with the iframe ready (`portal.onReady`; see [Getting started](./getting-started))
* An active wallet with collateral (USDC) and **gas** on the funding chain (see [Create a wallet](./create-a-wallet))
* Yield.xyz integration enabled in your Portal Dashboard (see [Yield.xyz Perps](/integrations/Yield/yield-xyz-perps))
* An `rpcConfig` entry for Arbitrum One (`eip155:42161`), the default `signingChainId` that EIP-712 signing requests are routed through, and for every chain an on-chain leg runs on. The default `rpcConfig` covers the [13 built-in chains](./getting-started#initializing-portal), Arbitrum One included. An explicit `rpcConfig` replaces the defaults rather than extending them, so keep `eip155:42161` in it.

<Warning>
  **There is no testnet for perps.** The Yield.xyz Perps API is production-only: every action is real money on mainnet. Perpetual futures are high-risk: leverage amplifies losses, and a position can be liquidated, losing its entire margin, within minutes.
</Warning>

<Warning>
  **Perps require a Portal client *without* [account abstraction](/resources/account-abstraction).** Hyperliquid verifies every action by recovering the signer from an EIP-712 signature, so funds deposited from a smart-contract wallet can be neither traded nor withdrawn. The Portal API rejects `createAction` and `submitTransaction` from an account abstraction enabled client with `INTEGRATION_REQUIRES_EOA_CLIENT`.
</Warning>

The examples in this guide use a `portal` instance:

```typescript theme={null}
import Portal from '@portal-hq/web'

const portal = new Portal({
  apiKey: 'YOUR_PORTAL_CLIENT_API_KEY',
  // Without rpcConfig, Portal's gateway serves the 13 built-in chains, Arbitrum One included.
})
```

Every account method (`createAction`, `getPositions`, `getOrders`, `getBalances`, `getActions`, `getActivity`, `getEvents`) takes an optional `address` that defaults to the Portal wallet's EVM address. A custom `address` is for the read methods: `signAndSubmitAction` signs only transactions built for the Portal wallet's address, so an action created for another address fails with `SIGNER_ADDRESS_MISMATCH`.

## Discovering venues

`getProviders` lists the trading venues, the actions each supports and the JSON schema of each action's `args`. `getProvider` returns one venue.

```typescript theme={null}
const { data: providers } = await portal.perps.yieldXyz.getProviders()

for (const provider of providers) {
  console.log(provider.id, provider.name, provider.supportedActions) // e.g. 'hyperliquid', 'Hyperliquid', ['open', 'close', ...]
}

const { data: hyperliquid } = await portal.perps.yieldXyz.getProvider('hyperliquid')
console.log(hyperliquid.argumentSchemas.open) // the arguments `open` accepts on this venue
```

Use the `providerId` values `getProviders` returns rather than a fixed list, and treat `argumentSchemas` as the source of truth for the `args` each action needs on a venue.

## Discovering markets

`getMarkets` lists the tradable markets, `getMarket` returns one by id, and `getMarketCandles` returns price candles for a chart.

```typescript theme={null}
const { data: page } = await portal.perps.yieldXyz.getMarkets({
  providerId: 'hyperliquid',
  sortBy: 'volume24h',
  order: 'desc',
  limit: 10,
})

for (const market of page.items) {
  console.log(market.id, market.markPrice, market.leverageRange, market.supportedMarginModes)
}

const { data: market } = await portal.perps.yieldXyz.getMarket('hyperliquid-eth-usdc')

const { data: candles } = await portal.perps.yieldXyz.getMarketCandles(market.id, {
  interval: '1h',
  from: Date.now() - 24 * 60 * 60 * 1000, // unix milliseconds
})
console.log(candles.candles.length)
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `providerId` | `string` | No | Only markets of this venue. |
| `sortBy` | `'volume24h' \| 'markPrice' \| 'priceChangePercent24h'` | No | Sort key. |
| `order` | `'asc' \| 'desc'` | No | Sort order. |
| `offset` | `number` | No | Pagination offset. |
| `limit` | `number` | No | Page size, from `1` to `100`. |

Each market carries `baseAsset` and `quoteAsset`, `leverageRange` (`[min, max]`), `supportedMarginModes`, `markPrice`, `oraclePrice`, `fundingRate` with `fundingRateIntervalHours`, `makerFee` and `takerFee`, `openInterest` and `minSize`. Treat market ids as opaque values returned by `getMarkets`; do not build them yourself. The examples use `hyperliquid-eth-usdc` for illustration; use an `id` that `getMarkets` returned. `getMarketCandles` requires an `interval` (`1m`, `5m`, `15m`, `1h`, `4h` or `1d`) and a `from` timestamp, with an optional `to`.

## Funding a trading account and approving the venue

A perps venue holds collateral in a **trading account**, separate from the wallet's on-chain balance. Before a user can open a position, fund that account and complete any one-time approvals the venue lists in its `supportedActions`.

Like every perps action, funding takes two calls: `createAction` builds the action with its unsigned transactions, and `signAndSubmitAction` signs, submits and tracks it (see [Signing and execution](#signing-and-execution)).

```typescript theme={null}
const { data: fund } = await portal.perps.yieldXyz.createAction({
  providerId: 'hyperliquid',
  action: 'fund',
  args: {
    amount: '10', // in source-token units: 10 USDC
    fromToken: {
      network: 'eip155:42161',
      address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC on Arbitrum One
    },
    fundingMethod: 'bridge2',
  },
})

await portal.perps.yieldXyz.signAndSubmitAction(fund)
```

<Warning>
  `bridge2` deposits below 5 USDC are not credited, and the funds are lost. Check the amount before you build a `bridge2` `fund` action.
</Warning>

`fund` returns on-chain `EVM_TRANSACTION` legs on the source chain (for example a USDC approval and the transfer), so the wallet needs gas there. `args.amount` is in units of the source token, not USD: from native ETH (`fromToken` without `address`) the same `'10'` means 10 ETH. `fundingMethod` selects the route: `bridge2` bridges USDC from Arbitrum One, and `lifi` routes from other EVM chains and tokens. See [Funding sources and limits](/integrations/Yield/yield-xyz-perps#funding-sources-and-limits) for the venue's limits.

<Note>
  A `fund` action reaches `SUCCESS` when the source-chain transaction confirms, **not** when the collateral lands on the venue. Poll `getBalances` until `availableBalance` reflects the deposit before you show a funded state.
</Note>

```typescript theme={null}
const { data: balance } = await portal.perps.yieldXyz.getBalances({ providerId: 'hyperliquid' })
console.log(balance.accountValue, balance.availableBalance)
```

Build the one-time approvals a venue lists in `supportedActions` the same way. Hyperliquid, for example, can place orders through an **agent wallet** the user authorizes once with `approveAgent`, and may require `approveBuilderFee`:

```typescript theme={null}
const { data: approval } = await portal.perps.yieldXyz.createAction({
  providerId: 'hyperliquid',
  action: 'approveAgent',
  args: { agentName: 'my-app' },
})

await portal.perps.yieldXyz.signAndSubmitAction(approval)
```

## Opening a position

Open a position with `action: 'open'`. The fields that size the order, and their units, are defined by the venue's `argumentSchemas.open`, so read that schema before you build the arguments.

```typescript theme={null}
import type { YieldXyzPerpsActionArgs } from '@portal-hq/web'

// `sizeArgs` holds the order size, in the field and units argumentSchemas.open defines for the venue.
async function openLong(sizeArgs: YieldXyzPerpsActionArgs) {
  const { data: open } = await portal.perps.yieldXyz.createAction({
    providerId: 'hyperliquid',
    action: 'open',
    args: {
      marketId: 'hyperliquid-eth-usdc',
      side: 'long',
      leverage: 5,
      marginMode: 'isolated',
      ...sizeArgs,
    },
  })

  // Show the summary before the user signs: Portal adds no trading guardrails.
  const summary = open.summary
  console.log(summary?.direction, summary?.size, summary?.leverage, summary?.collateral)
  console.log('Estimated liquidation price:', summary?.estimatedLiquidationPrice)

  return portal.perps.yieldXyz.signAndSubmitAction(open)
}
```

`createAction` takes:

| Parameter | Type | Required | Description |
| - | - | - | - |
| `providerId` | `string` | Yes | Venue id from `getProviders`. |
| `action` | `YieldXyzPerpsActionType` | Yes | `open`, `close`, `updateLeverage`, `stopLoss`, `takeProfit`, `setTpAndSl`, `cancelOrder`, `editOrder`, `updateMargin`, `fund`, `withdraw`, `approveAgent`, `approveBuilderFee` or `setUnifiedAccount`. |
| `args` | `YieldXyzPerpsActionArgs` | Yes | The action's arguments, sent as they are. The venue's `argumentSchemas` describes them. |
| `address` | `string` | No | Defaults to the Portal wallet's EVM address. |

`createAction` resolves with `{ data: action }`. The action carries its `id`, its `status`, a `summary` of the trade (`direction`, `size`, `leverage`, `collateral`, `fee`, `estimatedLiquidationPrice` and similar; simpler or `null` for non-trade actions such as `fund`) and its `transactions`, each with a `signingFormat` and `signablePayload`. Nothing is signed yet.

## Managing a position

Every management action follows the same `createAction` and `signAndSubmitAction` flow; only `action` and `args` differ. Each venue's `argumentSchemas` describes the exact `args`.

| Action | What it does | Key `args` |
| - | - | - |
| `updateLeverage` | Change a market's leverage | `marketId`, `leverage`, `marginMode` |
| `setTpAndSl` | Set or update stop-loss and take-profit together | `marketId`, `stopLossPrice`, `takeProfitPrice` |
| `stopLoss` | Set or update a stop-loss | `marketId`, `stopLossPrice` |
| `takeProfit` | Set or update a take-profit | `marketId`, `takeProfitPrice` |
| `updateMargin` | Add or remove margin on an isolated position | `marketId`, `amount` |
| `editOrder` | Adjust a resting order's price or size | `orderId`, `limitPrice`, `size` |
| `cancelOrder` | Cancel one or more resting orders | `orderId` or `orderIds` |
| `setUnifiedAccount` | Enable or disable unified-account mode where supported | `enabled` |

```typescript theme={null}
const { data: leverage } = await portal.perps.yieldXyz.createAction({
  providerId: 'hyperliquid',
  action: 'updateLeverage',
  args: { marketId: 'hyperliquid-eth-usdc', leverage: 2, marginMode: 'isolated' },
})
await portal.perps.yieldXyz.signAndSubmitAction(leverage)

const { data: protection } = await portal.perps.yieldXyz.createAction({
  providerId: 'hyperliquid',
  action: 'setTpAndSl',
  args: { marketId: 'hyperliquid-eth-usdc', stopLossPrice: 3600, takeProfitPrice: 4400 },
})
await portal.perps.yieldXyz.signAndSubmitAction(protection)
```

Each position and order also lists `pendingActions`, the actions available for it now, with pre-filled `args` you can pass to `createAction` (see [Positions, orders and balances](#positions-orders-and-balances)).

## Closing a position and withdrawing

Close a position with `action: 'close'`, then withdraw collateral from the trading account with `action: 'withdraw'`.

```typescript theme={null}
const { data: close } = await portal.perps.yieldXyz.createAction({
  providerId: 'hyperliquid',
  action: 'close',
  args: { marketId: 'hyperliquid-eth-usdc' },
})
await portal.perps.yieldXyz.signAndSubmitAction(close)

const { data: withdraw } = await portal.perps.yieldXyz.createAction({
  providerId: 'hyperliquid',
  action: 'withdraw',
  args: { amount: '100' },
})
await portal.perps.yieldXyz.signAndSubmitAction(withdraw)
```

On Hyperliquid, withdrawals return USDC on Arbitrum One to the signing address, with a flat fee and a minimum; see [Funding sources and limits](/integrations/Yield/yield-xyz-perps#funding-sources-and-limits).

## Positions, orders and balances

`getPositions` and `getOrders` take at least one venue (`providerId` or `providerIds`). `getBalances` takes exactly one venue and returns a single balance.

```typescript theme={null}
const { data: positions } = await portal.perps.yieldXyz.getPositions({ providerId: 'hyperliquid' })

for (const position of positions) {
  console.log(position.marketId, position.side, position.size, position.unrealizedPnl, position.liquidationPrice)
}

const { data: orders } = await portal.perps.yieldXyz.getOrders({ providerId: 'hyperliquid' })
const { data: balance } = await portal.perps.yieldXyz.getBalances({ providerId: 'hyperliquid' })
console.log(orders.length, balance.availableBalance)

const close = positions[0]?.pendingActions.find((pending) => pending.type === 'close')
if (close) {
  const { data: action } = await portal.perps.yieldXyz.createAction({
    providerId: 'hyperliquid',
    action: 'close',
    args: close.args,
  })
  await portal.perps.yieldXyz.signAndSubmitAction(action)
}
```

A position carries `marketId`, `side`, `size`, `entryPrice`, `markPrice`, `leverage`, `marginMode`, `margin`, `unrealizedPnl`, `funding`, `liquidationPrice` and `pendingActions`. A balance carries `accountValue`, `usedMargin`, `availableBalance` and `unrealizedPnl`. Calling `getPositions` or `getOrders` without a venue, or `getBalances` with none or several, throws before any request is sent.

## Signing and execution

### How an action is executed

`signAndSubmitAction(action, options?)` signs and submits an action's transactions, then tracks it:

1. It goes through the action's transactions in order. A transaction awaits a signature only while it is `CREATED`. Before signing one, the SDK reads the action back: a transaction Yield.xyz no longer lists as `CREATED` is skipped, and the payload signed is the one Yield.xyz lists now.
2. It signs each transaction according to its `signingFormat`:
   * **`EIP712_TYPED_DATA`** (Hyperliquid orders and account changes such as `updateLeverage`): signed off-chain with `eth_signTypedData_v4`, routed through `signingChainId` (Arbitrum One by default), and submitted as `signedPayload` with the action id. The typed data is signed as Yield.xyz returns it: its domain `chainId` (`1337` on Hyperliquid) is not rewritten and is never used for routing. The SDK only adds `types.EIP712Domain` when it is missing.
   * **`EVM_TRANSACTION`** (on-chain legs such as the `fund` approval and transfer): sent with `eth_sendTransaction` on the transaction's own chain, confirmed on-chain, then submitted as `transactionHash`.
3. It polls the action until it reaches a final status (`SUCCESS`, `FAILED`, `CANCELED` or `STALE`), signing any transaction Yield.xyz adds meanwhile, for example while the action is `WAITING_FOR_NEXT`.
4. It resolves when the action reaches `SUCCESS`. An action that ends `FAILED`, `CANCELED` or `STALE` throws `ACTION_FAILED`, and any other failure throws a [`YieldXyzPerpsExecutionError`](#handling-errors).

Yield.xyz reports a submitted transaction as `QUEUED`. Only `CREATED` transactions are signed, so running `signAndSubmitAction` again on the same action never re-signs a transaction Yield.xyz has already recorded. An on-chain leg broadcast without its hash being returned is not recorded yet and stays `CREATED`, so a blind re-run can broadcast it again (see [Resuming a failed execution](#resuming-a-failed-execution)).

Before signing, the SDK checks that the `signingFormat` is `EIP712_TYPED_DATA` or `EVM_TRANSACTION` (Solana and Cosmos payloads are not supported yet), that the transaction and an on-chain payload's `from` were built for the Portal wallet's EVM address, and that the chain the request is routed through has an `rpcConfig` entry. That chain check applies to per-call signers too. For on-chain legs, only `from`, `to`, `data`, `gasLimit` (sent as `gas`) and `value` are forwarded: the signer sets the nonce and fees.

Each signature goes through Portal's signing flow. Unless `autoApprove` is `true`, approve each request in your `portal_signingRequested` handler (see [Add custom signature hooks](./add-custom-signature-hooks)). A rejected request stops the execution with `SIGNATURE_REJECTED`. Without a `portal_signingRequested` handler, the request throws before anything is signed, so the execution stops with `SIGNING_FAILED`.

### Executing in one call

`executeAction(params, options?)` calls `createAction` and then `signAndSubmitAction`. Use the two-call flow when you want to show the `summary` before the user signs.

```typescript theme={null}
const result = await portal.perps.yieldXyz.executeAction(
  {
    providerId: 'hyperliquid',
    action: 'updateLeverage',
    args: { marketId: 'hyperliquid-eth-usdc', leverage: 1, marginMode: 'isolated' },
  },
  { onProgress: (event) => console.log(event.step, event.signingFormat ?? '') },
)
```

When `createAction` fails, `executeAction` throws the Portal API error as it is: no action exists yet, so there is nothing to resume.

### Options

Both methods take the same `YieldXyzPerpsExecuteOptions`:

| Name | Type | Required | Description |
| - | - | - | - |
| `onProgress` | `(event: YieldXyzPerpsExecutionProgress) => void` | No | Called at each execution step. An exception it throws is logged and does not affect the execution. |
| `signingChainId` | `string` | No | CAIP-2 chain that EIP-712 signing requests are routed through. Must have an `rpcConfig` entry. Defaults to `'eip155:42161'`. |
| `pollAction` | `boolean \| { intervalMs?: number; timeoutMs?: number }` | No | Whether to poll the action to a final status after its transactions are submitted. Defaults to `true`, with `intervalMs` `2000` and `timeoutMs` `600000`, which applies to each wait. With `false`, the method returns once the transactions listed as `CREATED` are submitted, and `status` is the action's current status. The action is still read back before each signature, so `TIMEOUT` can still occur. |
| `signTypedData` | `YieldXyzPerpsSignTypedData` | No | Per-call typed-data signer. |
| `signAndSendTransaction` | `YieldXyzPerpsSignAndSendTransaction` | No | Per-call on-chain transaction signer. |
| `waitForConfirmation` | `YieldXyzPerpsWaitForConfirmation` | No | Per-call confirmation waiter for on-chain legs. Only `true` counts as confirmed. Defaults to `portal.waitForConfirmation`. |
| `broadcastHashes` | `Record<string, string>` | No | On-chain legs an earlier attempt already broadcast, by transaction id. See [Resuming a failed execution](#resuming-a-failed-execution). |

### Progress events

`onProgress` receives a `YieldXyzPerpsExecutionProgress` with the `step`, the `actionId` and the fields that apply to that step:

| `step` | Meaning | Fields |
| - | - | - |
| `signing` | About to sign a transaction. | `transactionId`, `index`, `signingFormat` |
| `confirming` | An on-chain leg was broadcast; waiting for its receipt. | `transactionId`, `index`, `signingFormat`, `transactionHash` |
| `confirmed` | The on-chain leg confirmed. | `transactionId`, `index`, `signingFormat`, `transactionHash` |
| `submitted` | Yield.xyz accepted the signature or the transaction hash. | `transactionId`, `index`, `signingFormat`, plus `transactionHash` for on-chain legs |
| `polling` | The action was read back. | `actionStatus` |
| `failed` | Execution stopped. | `error`, plus `transactionId` and `transactionHash` when known |

An EIP-712 transaction goes `signing` → `submitted`; an on-chain leg goes `signing` → `confirming` → `confirmed` → `submitted`. `index` counts the transactions this call signed or submitted before this one.

```typescript theme={null}
import type { YieldXyzPerpsExecutionProgress } from '@portal-hq/web'

function onProgress(event: YieldXyzPerpsExecutionProgress) {
  switch (event.step) {
    case 'signing':
      console.log(`Signing ${event.signingFormat} transaction ${event.transactionId}`)
      break
    case 'confirming':
      console.log(`Waiting for ${event.transactionHash} to confirm`)
      break
    case 'submitted':
      console.log(`Submitted transaction ${event.transactionId}`)
      break
    case 'polling':
      console.log(`Action status: ${event.actionStatus}`)
      break
    case 'failed':
      console.error(`Stopped: ${event.error?.reason}`)
      break
  }
}

await portal.perps.yieldXyz.signAndSubmitAction(action, { onProgress })
```

### Return value

Both methods resolve with a `YieldXyzPerpsExecutionResult`:

| Field | Description |
| - | - |
| `actionId` | The action id. |
| `action` | The latest state of the action. |
| `status` | The action's final status (`SUCCESS`), or its current status with `pollAction: false`. |
| `transactions` | One entry per transaction this call signed or submitted: `transactionId`, `type`, `signingFormat`, `signedPayload` (EIP-712 transactions), `transactionHash` (on-chain legs) and `submitResult` (absent when the outcome was confirmed by reading the action back). |

### Using your own signers

Pass `signTypedData`, `signAndSendTransaction` or `waitForConfirmation` to replace one part of the signing, for example to add a confirmation step. The signing chain must still have an `rpcConfig` entry, and every transaction must have been built for the Portal wallet's EVM address.

```typescript theme={null}
import type { YieldXyzPerpsSignTypedData } from '@portal-hq/web'

const signTypedData: YieldXyzPerpsSignTypedData = async ({ address, typedData, chainId }) => {
  if (!window.confirm('Sign this Hyperliquid action?')) {
    return undefined // stops the execution with SIGNATURE_REJECTED
  }
  return portal.request({ chainId, method: 'eth_signTypedData_v4', params: [address, typedData] })
}

await portal.perps.yieldXyz.signAndSubmitAction(action, { signTypedData })
```

`signTypedData` receives the wallet `address`, the JSON-encoded typed data and the CAIP-2 `chainId` to route through, and resolves with the 65-byte signature. `signAndSendTransaction` receives the `eth_sendTransaction` params and the CAIP-2 chain id, and resolves with the transaction hash. Both resolve with `undefined` when the request is declined.

## Resuming a failed execution

A failed execution throws a `YieldXyzPerpsExecutionError` that carries the `actionId`, the `transactionId` and `action`, the latest known state of the action. Resume with `signAndSubmitAction(error.action)`, or with an action read back with `getAction`: transactions Yield.xyz already recorded are never signed again.

When the error carries a `broadcastHash`, that on-chain leg was already broadcast and Yield.xyz may not have recorded it yet. Pass it in `broadcastHashes`, keyed by transaction id, so the SDK confirms and submits that hash instead of signing and broadcasting the leg again.

```typescript theme={null}
import {
  YieldXyzPerpsExecutionError,
  YieldXyzPerpsExecutionErrorReason,
  type YieldXyzPerpsAction,
} from '@portal-hq/web'

const broadcastHashes: Record<string, string> = {}
let pending: YieldXyzPerpsAction = action

// Call once to execute, and again only when the user chooses to retry.
async function execute() {
  try {
    return await portal.perps.yieldXyz.signAndSubmitAction(pending, { broadcastHashes })
  } catch (error) {
    if (YieldXyzPerpsExecutionError.is(error)) {
      if (error.transactionId && error.broadcastHash) {
        broadcastHashes[error.transactionId] = error.broadcastHash
      }
      pending = error.action ?? pending
    }
    throw error
  }
}

try {
  await execute()
} catch (error) {
  if (!YieldXyzPerpsExecutionError.is(error)) throw error
  // A typed-data signature is submitted only after it is returned; an on-chain leg can be sent without its hash.
  const { transactionId } = error
  const failed = error.action?.transactions.find((transaction) => transaction.id === transactionId)
  if (
    error.reason === YieldXyzPerpsExecutionErrorReason.SigningFailed &&
    error.transactionId &&
    !error.broadcastHash &&
    failed?.signingFormat !== 'EIP712_TYPED_DATA'
  ) {
    // The on-chain leg may still have been sent. Don't offer a retry until you have checked the wallet's
    // transactions on that chain; if you find it, add its hash to broadcastHashes under error.transactionId.
  } else {
    // Persist `pending` and `broadcastHashes`, and call execute() again when the user retries.
    // The table below lists the reasons that can't be retried as is.
  }
}
```

Not every failure should be retried as is:

| Reason | What to do |
| - | - |
| `ACTION_FAILED` | The action ended as `FAILED`, `CANCELED` or `STALE`. It cannot be resumed; create a new action. |
| `ACTION_IN_PROGRESS` | Another call on this Portal instance is already executing the action. This call signed nothing. |
| `SIGNING_FAILED` | Signing or broadcasting failed. Check the action with `getAction` before retrying. For an on-chain leg (`EVM_TRANSACTION`) that is not enough: the leg may have been broadcast without its hash being returned, and that hash was never submitted to Yield.xyz. Check the wallet's transactions on that chain; if the leg was sent, resume with its hash in `broadcastHashes`, or a retry broadcasts it a second time. Without a `transactionId`, it is an unexpected error not tied to a specific transaction: check the action with `getAction` before retrying. |
| `SUBMIT_OUTCOME_UNKNOWN` | The submission failed ambiguously. Check the action before retrying; a signature is never resubmitted automatically. |
| `SUBMIT_REJECTED` with a `PERPS_SIGNATURE_*` API error | The Portal API could not verify the signature. This is a signing problem; retrying as is fails the same way. |
| `SUBMIT_FAILED` | Yield.xyz reported the transaction as `FAILED` after accepting the submission; a retry does not sign it again. For a typed-data (`EIP712_TYPED_DATA`) transaction, the venue rejected the signed request: create a new action. An on-chain leg (`EVM_TRANSACTION`) is submitted only after it confirmed on-chain, so it may already have taken effect. Check its effect first (for example the trading account with `getBalances`) before you create another action, or you may repeat it. |
| `TRANSACTION_NOT_CONFIRMED` | The on-chain leg was broadcast but did not confirm successfully: it was still pending when the wait ended, or it reverted. Check its receipt (`error.broadcastHash`). If it is still pending, resume: its hash is in `broadcastHashes`, so it is not sent again. If it reverted, the leg did not execute; create a new action, since resuming waits on the same reverted transaction. |
| `INVALID_SIGNATURE` | The signer returned something other than a 65-byte signature or a transaction hash, or `broadcastHashes` held an invalid hash; a retry with the same input fails the same way. For an on-chain leg the signer may already have broadcast the transaction, so check the wallet's transactions as for `SIGNING_FAILED`. |
| `UNSUPPORTED_SIGNING_FORMAT`, `INVALID_SIGNABLE_PAYLOAD`, `MISSING_WALLET_ADDRESS`, `SIGNER_ADDRESS_MISMATCH`, `SIGNING_CHAIN_NOT_CONFIGURED`, `DEPENDENCY_UNAVAILABLE` | Raised before the SDK signed anything in this call, but a plain retry fails the same way. Fix the cause (configuration such as `rpcConfig`, wallet or address) or create a new action. |

## Handling errors

Two kinds of error reach your app:

* **Portal API errors**, thrown by the read methods and `createAction`, and carried as `cause` by execution errors. On Web they are `PortalMpcError`s with the HTTP `status`, the stable error `id` and `details`. `getYieldXyzPerpsApiError(error)` returns the API error whether it was thrown directly or wrapped.
* **`YieldXyzPerpsExecutionError`**, thrown by `signAndSubmitAction` and `executeAction`. Check it with `YieldXyzPerpsExecutionError.is(error)` and branch on `reason`.

```typescript theme={null}
import {
  YieldXyzPerpsExecutionError,
  getYieldXyzPerpsApiError,
  isYieldXyzPerpsEoaRequiredError,
  isYieldXyzPerpsRateLimitError,
  isYieldXyzPerpsSchemaDriftError,
  isYieldXyzPerpsSignatureError,
} from '@portal-hq/web'

try {
  await portal.perps.yieldXyz.signAndSubmitAction(action)
} catch (error) {
  if (isYieldXyzPerpsEoaRequiredError(error)) {
    console.error('Perps needs a Portal client without account abstraction.')
  } else if (isYieldXyzPerpsSignatureError(error)) {
    console.error('The signature could not be verified; do not retry as is.')
  } else if (isYieldXyzPerpsSchemaDriftError(error)) {
    console.error('Yield.xyz Perps is temporarily unavailable.')
  } else if (isYieldXyzPerpsRateLimitError(error)) {
    console.error('Rate limited by Yield.xyz; try again shortly.')
  } else if (YieldXyzPerpsExecutionError.is(error)) {
    console.error(error.reason, error.actionId, error.transactionId, error.broadcastHash)
  }

  const apiError = getYieldXyzPerpsApiError(error)
  if (apiError) {
    console.error(apiError.status, apiError.id, apiError.message, apiError.details)
  }
}
```

The Portal API answers with `400` (invalid request, a precondition the venue rejected such as insufficient margin, a missing or rejected Yield.xyz API key, or one of the ids below), `404`, `429`, `503`, or `500` with `id: INTEGRATION_RESPONSE_SCHEMA_DRIFT`. When the venue returns a machine-readable code, it is in `details.code`. See [Errors](/integrations/Yield/yield-xyz-perps#errors) on the integration page. While executing, the SDK retries rate-limited (`429`) submissions and tolerates up to five consecutive transient failures while reading the action back. After an ambiguous submission failure, it resubmits a transaction hash once, and only if the action shows Yield.xyz did not record it; it never resubmits a signature. Signing is never retried.

`YieldXyzPerpsErrorId` values:

| Id | Helper | Meaning |
| - | - | - |
| `INTEGRATION_REQUIRES_EOA_CLIENT` | `isYieldXyzPerpsEoaRequiredError` | The client is account abstraction enabled; perps actions require an EOA client. |
| `PERPS_SIGNATURE_UNVERIFIABLE` | `isYieldXyzPerpsSignatureError` | The EIP-712 payload could not be hashed, so the signer could not be verified. |
| `PERPS_SIGNATURE_INVALID` | `isYieldXyzPerpsSignatureError` | No signer could be recovered from the signature. |
| `PERPS_SIGNATURE_ADDRESS_MISMATCH` | `isYieldXyzPerpsSignatureError` | The signature recovered to a different account than the action was built for. |
| `INTEGRATION_RESPONSE_SCHEMA_DRIFT` | `isYieldXyzPerpsSchemaDriftError` | Yield.xyz changed a response shape; treat it as a temporary outage of the feature. |

`YieldXyzPerpsExecutionErrorReason` values:

| Reason | Meaning |
| - | - |
| `UNSUPPORTED_SIGNING_FORMAT` | The transaction's `signingFormat` is missing or not supported (Solana and Cosmos are not supported yet). |
| `INVALID_SIGNABLE_PAYLOAD` | The `signablePayload` is missing or malformed, or does not identify an EVM chain. |
| `MISSING_WALLET_ADDRESS` | No EVM wallet address is available to sign with. |
| `SIGNER_ADDRESS_MISMATCH` | The transaction was built for an address other than the Portal wallet's EVM address. |
| `SIGNING_CHAIN_NOT_CONFIGURED` | The chain the signing request is routed through is not an EIP-155 chain with an `rpcConfig` entry. |
| `DEPENDENCY_UNAVAILABLE` | A signer or confirmation waiter needed for the transaction is not configured. |
| `SIGNATURE_REJECTED` | The signing request was rejected: no signature or hash was returned. |
| `INVALID_SIGNATURE` | The signer, or `broadcastHashes`, supplied a value that is not a 65-byte hex signature or a 32-byte transaction hash. |
| `SIGNING_FAILED` | Signing or broadcasting failed; `cause` holds the underlying error. Also used for unexpected errors. |
| `TRANSACTION_NOT_CONFIRMED` | An on-chain leg was broadcast but did not confirm successfully. |
| `SUBMIT_REJECTED` | The Portal API rejected the submission; `cause` holds the API error. |
| `SUBMIT_FAILED` | The submission was accepted, but Yield.xyz reported the transaction as `FAILED`. |
| `SUBMIT_OUTCOME_UNKNOWN` | The submission failed ambiguously and its outcome could not be confirmed. |
| `ACTION_FAILED` | The action ended as `FAILED`, `CANCELED` or `STALE`. |
| `ACTION_IN_PROGRESS` | Another call on this Portal instance is already executing the action. |
| `POLLING_FAILED` | The action could not be read back while tracking it; `cause` holds the error. |
| `TIMEOUT` | The action did not reach the awaited status before `pollAction.timeoutMs`. |

The error also carries `broadcastHash` (set once an on-chain leg was broadcast), `transactionHash` and `cause`.

## Activity and events

`getActions` lists the wallet's actions (filter by `providerId`, `marketId`, `type`, `status` or `statuses`) and `getAction` reads one. `getEvents` lists venue-side outcomes the user did not initiate, such as order fills, liquidations and stop-loss or take-profit triggers, and `getEvent` reads one. `getActivity` interleaves actions and events in a single feed, newest first.

```typescript theme={null}
const { data: actions } = await portal.perps.yieldXyz.getActions({ providerId: 'hyperliquid', limit: 20 })
for (const item of actions.items) {
  console.log(item.id, item.action, item.status, item.completedAt)
}

// Read one action back by id, for example the one createAction returned
const { data: current } = await portal.perps.yieldXyz.getAction(action.id)
console.log(current.status, current.completedAt)

const { data: events } = await portal.perps.yieldXyz.getEvents({
  providerId: 'hyperliquid',
  eventTypes: ['order_filled', 'liquidation'],
})
const [firstEvent] = events.items
if (firstEvent) {
  const { data: event } = await portal.perps.yieldXyz.getEvent(firstEvent.id)
  console.log(event.eventType, event.occurredAt, event.order.marketId)
}

const { data: activity } = await portal.perps.yieldXyz.getActivity({ providerId: 'hyperliquid', limit: 20 })
for (const item of activity.items) {
  if (item.type === 'event') {
    console.log('event', item.event.eventType)
  } else {
    console.log('action', item.action.action, item.action.status)
  }
}
```

The list methods take `offset` and `limit` (from `1` to `100`) and resolve with a page: `{ data: { total, offset, limit, items } }`. `getEvents` also filters by `eventType` or `eventTypes`, `marketId`, `perpActionId`, `providerOrderId` and `fromDate` / `toDate` (ISO-8601).

## Low-level APIs

`signAndSubmitAction` covers signing, submitting and tracking. If you sign the transactions yourself, use the low-level methods with the [Enclave MPC API signing flow](/apis/enclave-mpc/guide/yield-xyz-perps#transaction-processing) as the reference:

| Method | Description |
| - | - |
| `submitTransaction(params)` | Submits `{ transactionId, actionId, signedPayload }` for a signature (the Portal API needs `actionId` to verify the signer), or `{ transactionId, transactionHash }` for an on-chain leg you broadcast. Resolves with `{ data: { status, link, transactionHash, error, details } }`; a `status` of `FAILED` means the venue rejected the transaction even though the request succeeded. |
| `getHealth()` | Returns the integration's health: `{ data: { status: 'OK' \| 'FAIL', timestamp } }`. |
| `getProvidersHealth()` | Returns each venue's state: `{ data: { generatedAt, providers: [{ providerId, state, message, setAt }] } }`, where `state` is `healthy`, `degraded` or `down`. |

```typescript theme={null}
const transaction = action.transactions[0]

const { data: submitted } = await portal.perps.yieldXyz.submitTransaction({
  transactionId: transaction.id,
  actionId: action.id,
  signedPayload: '0x...', // the EIP-712 signature
})
console.log(submitted.status, submitted.link)
```

`submitTransaction` throws before any request is sent when neither `signedPayload` nor `transactionHash` is given, or when `signedPayload` comes without `actionId`. There is no step method in Perps: poll `getAction` until the action reaches a final status, and sign the transactions Yield.xyz adds while it is `WAITING_FOR_NEXT`.

## Best practices

1. **Show the `summary`** (`direction`, `size`, `leverage`, `collateral`, `estimatedLiquidationPrice`) before the user signs: Portal adds no trading guardrails
2. **Fund and approve first**: a position cannot open until the trading account has collateral and the venue's approvals are complete
3. **Respect the venue's limits** before building an action, for example Hyperliquid's 5 USDC `bridge2` minimum and its minimum order notional
4. **Sign soon after building**: on-chain legs carry the gas limit Yield.xyz estimated when the action was built, so create a new action if the user waits long before signing
5. **Persist `error.action` and `broadcastHashes`** so a failed execution can be resumed without broadcasting an on-chain leg twice
6. **Handle `INTEGRATION_RESPONSE_SCHEMA_DRIFT`** as a feature outage rather than a user error
7. **Keep gas on the wallet** for the on-chain funding legs

## Supported venues

Perps launches on Hyperliquid; the exact list of live venues is returned by `getProviders`. On-chain funding legs settle on real chains (for example Arbitrum One, `eip155:42161`), and each needs an `rpcConfig` entry to be signed.

## Next Steps

* Set up the integration: [Yield.xyz Perps](/integrations/Yield/yield-xyz-perps)
* Use the [Enclave MPC API](/apis/enclave-mpc/guide/yield-xyz-perps) to trade perps from your backend
* Check out the [Yield.xyz Perps API reference](/api-reference/yieldxyz-perps/list-trading-venues)
* Approve signing requests with [custom signature hooks](./add-custom-signature-hooks)


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