# Message Signing

Meridian uses EIP-712 signatures to authorize orders, cancellations, withdrawals, token conversions, and linked-signer changes. The private key stays in the client.

## Get the Signing Configuration

Get the current domain and message types from the same API environment that will receive the request:

```http
GET /v1/rpc/config
```

The response contains:

* `domain.name`
* `domain.version`
* `domain.chainId`
* `domain.verifyingContract`
* `signatureTypes`

Use every domain value exactly as returned by Meridian. Do not substitute another domain name. A different value invalidates the signature.

Do not hard-code a type definition. Parse the applicable string from `signatureTypes`. Contract upgrades can change the current domain or type.

## Time Fields

Signed actions use two time values:

| Field      | Unit        | API format     | Purpose                          |
| ---------- | ----------- | -------------- | -------------------------------- |
| `nonce`    | Nanoseconds | Decimal string | Uniqueness and replay protection |
| `signedAt` | Seconds     | JSON integer   | Message freshness                |

The API accepts a nonce only when it is within 25 minutes of server time. It accepts `signedAt` from 1 hour in the past to 10 seconds in the future.

Get server time from `GET /v1/time`. Synchronize the client clock. Do not reuse a nonce for the same action type.

This browser-compatible helper adds a random sub-millisecond component:

```typescript
const getNonce = (): bigint => {
  const random = crypto.getRandomValues(new Uint32Array(1))[0] % 1_000_000
  return BigInt(Date.now()) * 1_000_000n + BigInt(random)
}

const getSignedAt = (): number => Math.floor(Date.now() / 1_000)
```

Send `nonce` in JSON as a string. A nanosecond value is larger than the safe integer range in JavaScript.

## Decimal Values

Signed prices, quantities, withdrawal amounts, and conversion amounts use 9-decimal fixed-point integers. The JSON request uses decimal strings.

Keep the source value as a string. Derive the signed integer and JSON value from the same string:

```typescript
import { parseUnits } from 'viem'

const quantity = '5.5'
const signedQuantity = parseUnits(quantity, 9) // 5500000000n
```

Do not use JavaScript floating-point arithmetic for a price or quantity. A rounding difference changes the signed message hash.

## Subaccount Encoding

Signed messages identify a subaccount with a `bytes32` name. Encode a short UTF-8 name and right-pad it with zero bytes:

```typescript
import { stringToHex } from 'viem'

const subaccount = stringToHex('primary', { size: 32 })
```

API queries usually identify the same subaccount by its UUID. Do not put the UUID in an EIP-712 `subaccount` field.

## Limit-Order Example

This example gets the live signing configuration, signs one limit order, and submits it. It uses the current type string from the API.

The example targets testnet. For mainnet, set `API_BASE` to `https://api.meridian.xyz/v1` and use a mainnet key. The signing domain returned by each host is different, so never reuse a signature across environments.

```typescript
import { createWalletClient, http, parseAbiParameters, parseUnits, stringToHex, type Address, type Hex } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'

const API_BASE = 'https://api.meridiantest.net/v1'
const account = privateKeyToAccount('0xYOUR_TEST_PRIVATE_KEY' as Hex)
const wallet = createWalletClient({ account, transport: http() })

const getNonce = (): bigint => {
  const random = crypto.getRandomValues(new Uint32Array(1))[0] % 1_000_000
  return BigInt(Date.now()) * 1_000_000n + BigInt(random)
}

const getSignedAt = (): number => Math.floor(Date.now() / 1_000)

const configResponse = await fetch(`${API_BASE}/rpc/config`)
if (!configResponse.ok) throw new Error(`Config request failed: ${configResponse.status}`)

const config = (await configResponse.json()) as {
  domain: {
    name: string
    version: string
    chainId: number
    verifyingContract: Address
  }
  signatureTypes: { TradeOrder: string }
}

const tradeOrderTypes = {
  TradeOrder: parseAbiParameters(config.signatureTypes.TradeOrder)
}

const quantity = '5.5'
const price = '4200.5'
const nonce = getNonce()
const signedAt = getSignedAt()
const subaccount = stringToHex('primary', { size: 32 })

const signature = await wallet.signTypedData({
  account,
  domain: config.domain,
  types: tradeOrderTypes,
  primaryType: 'TradeOrder',
  message: {
    sender: account.address,
    subaccount,
    quantity: parseUnits(quantity, 9),
    price: parseUnits(price, 9),
    reduceOnly: false,
    side: 0, // BUY
    engineType: 0, // PERP
    productId: 1,
    nonce,
    signedAt: BigInt(signedAt)
  }
})

const orderResponse = await fetch(`${API_BASE}/order`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    data: {
      sender: account.address,
      subaccount,
      quantity,
      price,
      reduceOnly: false,
      side: 0,
      engineType: 0,
      onchainId: 1,
      type: 'LIMIT',
      timeInForce: 'GTD',
      postOnly: false,
      nonce: nonce.toString(),
      signedAt
    },
    signature
  })
})

if (!orderResponse.ok) {
  throw new Error(`Order request failed: ${orderResponse.status} ${await orderResponse.text()}`)
}
```

Get `productId` from the product's `onchainId` field. The EIP-712 message calls this value `productId`, but the JSON request calls it `onchainId`.

For a market order, sign `price` as `0n`. Set `type` to `MARKET` and omit `price` from the JSON body.

## Signed Action Types

`GET /v1/rpc/config` returns the authoritative field order and integer sizes for these types:

| Type                  | Use                                      |
| --------------------- | ---------------------------------------- |
| `TradeOrder`          | Submit an order                          |
| `CancelOrder`         | Cancel one or more orders                |
| `LinkSigner`          | Link a delegated signer                  |
| `RevokeLinkedSigner`  | Revoke a delegated signer                |
| `RefreshLinkedSigner` | Restore or refresh a signer as the owner |
| `ExtendLinkedSigner`  | Extend a signer as that signer           |
| `InitiateWithdraw`    | Authorize a withdrawal                   |
| `ConvertToken`        | Convert a compatible quote-token balance |
| `EIP712Auth`          | Authenticate a protected read or action  |

Some types are verified only by the API. Other types are also verified onchain. Sign both groups with the returned EIP-712 domain.

## Cancel Orders

`CancelOrder` signs only `sender`, `subaccount`, and `nonce`. Put the target `orderIds`, `clientOrderIds`, or both in the JSON request to `POST /v1/order/cancel`.

One request must identify at least one order. The combined number of IDs cannot exceed 200.

To cancel every open order for the subaccount, send the same signed fields to `POST /v1/order/cancel-all` and omit both ID arrays. Use a fresh nonce for each cancel or cancel-all request.

See [Order Placement](/developer-guides/trading-api/order-placement#cancel-all-orders) for cancel-all scope and response behavior.

## Linked Signers

For an owner-signed action, `sender` is the owner address. For an action that an active linked signer authorizes, `sender` is the linked-signer address.

A linked signer can sign orders, cancellations, and supported internal token conversions for its subaccount. It cannot authorize a withdrawal.

`LinkSigner` requires two signatures over the same message:

* `signature` from the owner.
* `signerSignature` from the new linked signer.

See [Accounts and Signers](/developer-guides/trading-api/accounts-and-signers) for expiry, renewal, and revocation rules.

## Smart-Contract Wallets

An account owner can use an EIP-1271 smart-contract wallet. The contract validates its signature through `isValidSignature`. A linked signer must be an externally owned account.

## Troubleshooting

If verification fails, check these items in order:

1. Get a fresh domain and type string from the target environment.
2. Check that the signer address equals the message `sender` or `account` field.
3. Check that `nonce` is a nanosecond string and `signedAt` is a seconds integer.
4. Check that the subaccount is a 32-byte name, not a UUID.
5. Check that all signed decimal values use 9-decimal fixed-point integers.
6. For a market order, check that the signed price is zero.
7. For a linked signer, check its subaccount and `expiresAt` value.

Use the [interactive API reference](/protocol-reference/api-hosts) for the current request schema of each endpoint.
