# 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.

```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.
