Skip to content

Submit & Track

Submit an intent

http
POST /v2/trading/submit-intent?blockchain=MONAD
Content-Type: application/json
json
{
  "trader":      "0x…",
  "action":      0,
  "nonce":       "1785312000123",
  "deadline":    1785312300,
  "feeToken":    "0x…",
  "antiDdosFee": "500000",
  "actionData":  "0x…",
  "signature":   "0x…"
}

Returns 200 with the intent hash as a JSON string, e.g. "0xabc123…".

WARNING

nonce and antiDdosFee must be strings. nonce is a millisecond timestamp that exceeds Number.MAX_SAFE_INTEGER; sending it as a JSON number silently corrupts it and the signature check fails.

ts
const res = await fetch(`${baseUrl}/v2/trading/submit-intent?blockchain=MONAD`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    trader,
    action,
    nonce: nonce.toString(),
    deadline,
    feeToken,
    antiDdosFee: antiDdosFee.toString(),
    actionData,
    signature,
  }),
})

if (!res.ok) throw new Error(`submit-intent ${res.status}: ${await res.text()}`)

const intentHash = (await res.text()).replace(/^"|"$/g, '')

The intent hash

Computed identically by the backend and the contract:

solidity
intentHash = keccak256(abi.encode(trader, action, nonce, deadline, actionData));

It excludes feeToken, antiDdosFee and signature, and is not the EIP-712 digest. Use the value returned by the response rather than deriving it yourself.

Validation

The endpoint checks the following before queueing. Any failure returns 400, except the position lookup, which returns 404.

CheckError text contains
deadline in the futuredeadline already passed
nonce not older than 2 daysnonce too old
nonce not more than 5 minutes aheadnonce too far in future
action in [0, 13]invalid action
trader / feeToken are valid addressesinvalid … address
actionData / signature non-emptymust not be empty
(action, feeToken, antiDdosFee) matches current or previous configantiDdosFee mismatch, feeToken not supported, anti-ddos not enabled
actionData decodes under the action's ABIdecode error
For actions whose oracle feeds are resolved from a stored position, the position is knownPosition not found (404)

The signature is not checked here

Signature validity and agent permissions are enforced onchain, not at submission. An intent with a bad signature or an unauthorized agent is accepted with 200 and later skipped with skipReason = INVALID. Make sure the agent is authorized and holds the right permission bit before you rely on a 200.

Position not found

Actions whose oracle feeds the relayer resolves by looking the position up require that position to be indexed — MARKET_CLOSE, BATCH_MARKET_CLOSE, PARTIAL_CLOSE, ADD_MARGIN, REMOVE_MARGIN, BATCH_CREATE_DECREASE_ORDERS. Closing right after opening can hit this. Retry with backoff, or confirm the position exists via getPositionsV4 first.

Opens are unaffected — they carry the market in actionData. Order-only actions are also exempt, including CANCEL_ALL_DECREASE_ORDERS, which takes a positionHash but needs no lookup. See Actions Reference.

This is the only submission failure that is not a 400.

Poll status

http
GET /v2/trading/{intent_hash}/status
json
{
  "submitted":  true,
  "executed":   true,
  "success":    true,
  "skipped":    false,
  "skipReason": null,
  "txnHash":    "0x…",
  "reason":     "0x"
}

State machine:

POST accepted
  → submitted = false                      queued, waiting to be relayed
  → submitted = true,  executed = false    broadcast, waiting for the block
  → executed = true,   success = true|false        settled onchain
  or skipped = true,   skipReason = INVALID|NONCE|FEE|UNKNOWN

An intent can be skipped either because pre-flight simulation rejected it — in which case it never goes onchain and costs nothing — or because onchain execution rejected it. The response fields look the same in both cases.

Polling strategy

A two-phase poll gives the best user experience:

Phase 1 — wait for `submitted` (300ms interval, 20s timeout)
  - a terminal state during this window is returned immediately
  - on submitted = true, report optimistic success and let the user continue

Phase 2 — keep polling for the terminal state in the background
  (300ms interval, 5 minute timeout, matching the intent deadline)
  - surface an error if it ends in failure
ts
export async function pollIntentStatus(
  baseUrl: string,
  intentHash: string,
  { interval = 300, timeout = 300_000 } = {},
): Promise<IntentStatus> {
  const until = Date.now() + timeout

  while (Date.now() < until) {
    const res = await fetch(`${baseUrl}/v2/trading/${intentHash}/status`)
    if (res.ok) {
      const status: IntentStatus = await res.json()
      if (status.executed || status.skipped) return status
    }
    await new Promise((r) => setTimeout(r, interval))
  }

  throw new Error('Timed out waiting for intent execution')
}

Interpreting the result

StateMeaningWhat to do
submitted = falseQueuedKeep waiting
submitted = true, executed = falseBroadcastKeep waiting
executed = true, success = trueDoneRefresh positions and orders
executed = true, success = falseExecuted but reverted; reason holds the revert dataDecode and show; retry with a new nonce
skipped, skipReason = INVALIDBad signature, unauthorized agent, missing permission bit, or expired deadlineCheck authorization, deadline, EIP-712 structure and primaryType
skipped, skipReason = NONCENonce not increasing, or outside the windowRe-sign with a larger nonce
skipped, skipReason = FEEFee could not be collectedCheck the fee token balance and Diamond allowance
skipped, skipReason = UNKNOWNUnclassifiedReport it

Decoding reason

reason is the raw revert data from the chain. Decode in this order:

  1. A skipReason enum value → map directly to a message.
  2. Standard Error(string) / Panic(uint256).
  3. Protocol custom errors — see Error Reference.
  4. Fall back to a generic message.
ts
import { decodeErrorResult } from 'viem'

const SOLIDITY_ERROR_ABI = [
  { type: 'error', name: 'Error', inputs: [{ name: 'message', type: 'string' }] },
  { type: 'error', name: 'Panic', inputs: [{ name: 'code', type: 'uint256' }] },
] as const

function decodeReason(reason: string): string {
  if (!reason || reason === '0x') return ''

  const skip = SKIP_REASON_MESSAGES[reason.toUpperCase()]
  if (skip) return skip

  try {
    const decoded = decodeErrorResult({ abi: SOLIDITY_ERROR_ABI, data: reason as `0x${string}` })
    if (decoded.errorName === 'Error') return String(decoded.args?.[0] ?? '')
  } catch {
    // fall through to protocol custom errors
  }

  return decodeCustomError(reason) ?? 'The transaction could not be executed.'
}

Retrying

A skipped or reverted intent is not retried for you. To retry:

  1. Sign again with a new nonce — never resubmit the same payload.
  2. Refresh the fee configuration if more than ~120 seconds have passed.
  3. Refresh the market price and slippage bound if the action is an open.

Common integration errors

SymptomLikely cause
Persistent INVALID with a plausible signatureEIP-712 struct expands business fields instead of using actionDataHash; wrong primaryType; verifyingContract is not the Diamond
Static actions work, batch actions (8/11/12) decode as garbageactionData missing the trader prefix — see Building actionData
antiDdosFee mismatchFee config cache older than ~120 seconds, or a disabled fee token
FEE skips despite sufficient balanceMissing Diamond approval, or the trade's own spend was not added to the required amount
Occasional NONCE under loadTwo intents signed in the same millisecond
Signature fails after a working prototypenonce sent as a JSON number instead of a string

Next: Reference Client.

Trading perpetuals involves risk. Nothing here is financial advice.