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

# How to Sign Upside Write Requests with EIP-712 ECDSA

> Learn how to sign POST /exchange requests using EIP-712 structured-data signatures over secp256k1. The server recovers your identity from the signature — no API keys needed.

Every `POST /exchange` request must include a valid **EIP-712** structured-data signature (secp256k1 ECDSA) over the action payload. The server recovers your Ethereum address from the signature at request time — there are no API keys, bearer tokens, or sessions to manage. If the recovered address matches a registered account, the operation proceeds. If it doesn't, the server returns `SIGNATURE_INVALID`.

## Domain

All signatures use a fixed EIP-712 domain:

| Field               | Value                                        |
| ------------------- | -------------------------------------------- |
| `name`              | `"Exchange"`                                 |
| `version`           | `"1"`                                        |
| `chainId`           | `9767`                                       |
| `verifyingContract` | `0x0000000000000000000000000000000000000000` |

## Two Signing Paths

The signing struct is chosen by `action.type`:

<CardGroup cols={2}>
  <Card title="Agent path" icon="robot">
    Trading and programmatic actions — `order`, `cancel`, `cancelByCloid`, `modify`, and most others. The entire canonical action is folded into a single `actionHash` carried by a generic `Agent(string source, bytes32 actionHash)` struct.
  </Card>

  <Card title="Typed path" icon="file-signature">
    Funds and permission actions — `registerAccount`, `approveAgent`, `revokeAgent`, `lockCollateral`, `unlockCollateral`, `transferBetweenDeployers`. Each has a field-level EIP-712 struct so a wallet can render human-readable values for review.
  </Card>
</CardGroup>

<Note>
  Both paths share the same domain, digest formula, and request envelope. Only the `hashStruct` differs. If an action type is not in the typed list above, use the Agent path.
</Note>

## Agent Path

Used for `order`, `cancel`, `cancelByCloid`, `cancelAll`, `modify`, `updateIsolatedMargin`, `updateLeverage`, `updateMarginMode`, `updateFeeSetting`, `tpSl`, `cancelTpSl`, `cancelConditional`, `enrollUserToMarketDeployer`, `setMarginShareType`, and the share-group transfer actions.

<Steps>
  <Step title="Build the action object">
    Construct the action as a plain JSON object. Every action has a `type` field that identifies the operation.

    ```json theme={"system"}
    {
      "type": "order",
      "orders": [
        { "a": 1, "b": true, "p": "50", "s": "10", "r": false, "t": { "limit": { "tif": "Gtc" } } }
      ],
      "grouping": "na"
    }
    ```
  </Step>

  <Step title="Serialize to canonical JSON">
    Serialize the action to canonical JSON: **sort keys alphabetically**, use compact separators (`,` and `:`) with **no whitespace**, and exclude `undefined`/`null` fields. In Python this is exactly `json.dumps(action, sort_keys=True, separators=(",", ":"))`.

    ```text theme={"system"}
    Input:  { "type": "order", "grouping": "na", "orders": [...] }
    Output: {"grouping":"na","orders":[...],"type":"order"}
    ```

    Key ordering is crucial. The server re-serializes the action using the same rules; any mismatch produces a different hash and `SIGNATURE_INVALID`.
  </Step>

  <Step title="Compute the actionHash">
    Concatenate the canonical JSON bytes with the nonce encoded as a **big-endian 8-byte** value, then keccak256:

    ```python theme={"system"}
    from eth_utils import keccak

    canonical = json.dumps(action, sort_keys=True, separators=(",", ":")).encode()
    action_hash = keccak(canonical + int(nonce).to_bytes(8, "big"))
    ```
  </Step>

  <Step title="Build the hashStruct">
    Wrap the `actionHash` in the generic `Agent` struct with `source = "b"`:

    ```python theme={"system"}
    hash_struct = keccak(
        keccak(b"Agent(string source,bytes32 actionHash)")
        + keccak(b"b")          # keccak256 of the source string "b"
        + action_hash
    )
    ```
  </Step>

  <Step title="Compute the EIP-712 digest and sign">
    See [Digest & signature](#digest--signature) below — this step is identical for both paths.
  </Step>
</Steps>

## Typed Path

Used for `registerAccount`, `approveAgent`, `revokeAgent`, `lockCollateral`, `unlockCollateral`, and `transferBetweenDeployers`. Each action encodes its fields directly into a struct — there is **no** canonical JSON step.

The `hashStruct` is `keccak256( typeHash(<Struct>) ‖ enc(field1) ‖ … ‖ enc(nonce) )`, where each field is encoded as:

| Solidity type | Encoding                    |
| ------------- | --------------------------- |
| `string`      | `keccak256(utf8Bytes)`      |
| `address`     | 20 bytes, left-padded to 32 |
| `uintN`       | big-endian, 32 bytes        |

Struct definitions (the `nonce` is always the final `uint64` field):

| Action                     | Struct                                                                                                                     |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `registerAccount`          | `RegisterAccount(address address,uint64 nonce)`                                                                            |
| `approveAgent`             | `ApproveAgent(address agentAddress,string agentName,uint64 validUntil,uint64 nonce)`                                       |
| `revokeAgent`              | `RevokeAgent(address agentAddress,uint64 nonce)`                                                                           |
| `lockCollateral`           | `LockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)`                                         |
| `unlockCollateral`         | `UnlockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)`                                       |
| `transferBetweenDeployers` | `TransferBetweenDeployers(uint32 fromMarketDeployerId,uint32 toMarketDeployerId,uint32 coinId,string amount,uint64 nonce)` |

```python theme={"system"}
# Example: registerAccount
hash_struct = keccak(
    keccak(b"RegisterAccount(address address,uint64 nonce)")
    + (b"\x00" * 12 + bytes.fromhex(address[2:]))   # address → 32 bytes
    + int(nonce).to_bytes(32, "big")                # uint64 nonce → 32 bytes
)
```

## Digest & Signature

Both paths finish identically. Build the domain separator, form the EIP-712 digest, and sign it directly.

<Steps>
  <Step title="Domain separator">
    ```text theme={"system"}
    domainSeparator = keccak256(
        typeHash(EIP712Domain)
        ‖ keccak256("Exchange") ‖ keccak256("1")
        ‖ uint256(9767) ‖ uint256(0)
    )
    ```

    where `typeHash(EIP712Domain) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`.
  </Step>

  <Step title="EIP-712 digest">
    ```text theme={"system"}
    digest = keccak256( 0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct )   // 32 bytes
    ```

    <Warning>
      Use **Ethereum keccak256**, not NIST SHA-3. The `digest` is already the final hash — sign it directly (`prehash = false`). Never hash it again.
    </Warning>
  </Step>

  <Step title="Sign the digest">
    Sign the 32-byte digest with secp256k1 (RFC 6979 deterministic `k`). Set `v = 27 + recovery_bit`.

    ```python theme={"system"}
    from eth_account import Account

    sig = Account._sign_hash(digest, PRIVATE_KEY)
    r = "0x" + sig.r.to_bytes(32, "big").hex()
    s = "0x" + sig.s.to_bytes(32, "big").hex()
    v = sig.v if sig.v >= 27 else sig.v + 27   # 27 or 28
    ```
  </Step>
</Steps>

## Signature Format

Include the signature in the `signature` field of the request envelope:

```json theme={"system"}
{
  "r": "0x<64 lowercase hex characters>",
  "s": "0x<64 lowercase hex characters>",
  "v": 27
}
```

Both `r` and `s` are 32-byte values as `0x`-prefixed lowercase hex strings (66 characters including the prefix). `v` is `27` or `28`.

## Request Envelope

The full `POST /exchange` body is the same for both paths:

```json theme={"system"}
{
  "action": {
    "type": "order",
    "orders": [{ "a": 1, "b": true, "p": "50", "s": "10", "r": false, "t": { "limit": { "tif": "Gtc" } } }],
    "grouping": "na"
  },
  "signature": { "r": "0x3b2a...", "s": "0x7cf1...", "v": 28 },
  "nonce": 1778572951477
}
```

<Note>
  An optional `vaultAddress` field can be included at the top level of the envelope for vault proxy operations. When present, the server verifies that the signer is an authorized agent of the specified vault.
</Note>

## Complete Signing Helper

A reusable helper that implements both paths. This is the reference implementation — copy, paste, and run.

```python theme={"system"}
import json, time, requests
from eth_account import Account
from eth_utils import keccak

BASE_URL = "https://dev.upsidemax.xyz"

# EIP-712 domain: Exchange / v1 / chainId 9767 / verifyingContract 0x0
_CHAIN_ID, _NAME, _VER, _SOURCE = 9767, "Exchange", "1", "b"

def _u(v):    return int(v).to_bytes(32, "big")
def _addr(s): return b"\x00" * 12 + bytes.fromhex(s[2:] if s[:2].lower() == "0x" else s)
def _s(v):    return keccak(str(v).encode())

_DOMAIN = keccak(
    keccak(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
    + keccak(_NAME.encode()) + keccak(_VER.encode()) + _u(_CHAIN_ID) + b"\x00" * 32)

# Funds / permission actions use a field-level typed struct; everything else uses the Agent path.
_TYPED = {
    "registerAccount": (b"RegisterAccount(address address,uint64 nonce)",
        lambda a, n: [_addr(a["address"]), _u(n)]),
    "approveAgent": (b"ApproveAgent(address agentAddress,string agentName,uint64 validUntil,uint64 nonce)",
        lambda a, n: [_addr(a["agentAddress"]), _s(a["agentName"]), _u(a["validUntil"]), _u(n)]),
    "revokeAgent": (b"RevokeAgent(address agentAddress,uint64 nonce)",
        lambda a, n: [_addr(a["agentAddress"]), _u(n)]),
    "lockCollateral": (b"LockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)",
        lambda a, n: [_u(a["marketDeployerId"]), _u(a["coinId"]), _s(a["amount"]), _u(n)]),
    "unlockCollateral": (b"UnlockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)",
        lambda a, n: [_u(a["marketDeployerId"]), _u(a["coinId"]), _s(a["amount"]), _u(n)]),
    "transferBetweenDeployers": (b"TransferBetweenDeployers(uint32 fromMarketDeployerId,uint32 toMarketDeployerId,uint32 coinId,string amount,uint64 nonce)",
        lambda a, n: [_u(a["fromMarketDeployerId"]), _u(a["toMarketDeployerId"]), _u(a["coinId"]), _s(a["amount"]), _u(n)]),
}

def eip712_digest(action, nonce):
    typed = _TYPED.get(action["type"])
    if typed:  # typed path — field-level EIP-712 encoding
        type_str, build = typed
        struct = keccak(keccak(type_str) + b"".join(build(action, nonce)))
    else:      # Agent path — canonical JSON folded into an actionHash
        canonical = json.dumps(action, sort_keys=True, separators=(",", ":")).encode()
        action_hash = keccak(canonical + int(nonce).to_bytes(8, "big"))
        struct = keccak(keccak(b"Agent(string source,bytes32 actionHash)")
                        + keccak(_SOURCE.encode()) + action_hash)
    return keccak(b"\x19\x01" + _DOMAIN + struct)

def sign_and_send(action, private_key):
    """Sign an action with EIP-712 and POST it to /exchange."""
    nonce = int(time.time() * 1000)
    sig = Account._sign_hash(eip712_digest(action, nonce), private_key)
    envelope = {
        "action": action,
        "signature": {
            "r": "0x" + sig.r.to_bytes(32, "big").hex(),
            "s": "0x" + sig.s.to_bytes(32, "big").hex(),
            "v": sig.v if sig.v >= 27 else sig.v + 27,
        },
        "nonce": nonce,
    }
    resp = requests.post(f"{BASE_URL}/exchange", json=envelope, timeout=15)
    return resp.json()
```

## Common Errors

| Error                            | Typical Cause                                                                                                                                                                                                                                                                                                 |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SIGNATURE_INVALID`              | Recovered address doesn't match the registered account. Common causes: unsorted JSON keys or whitespace in canonical JSON (Agent path), wrong domain constants, wrong nonce byte-width (8 bytes in the Agent `actionHash` vs 32 bytes as a typed field), double-hashing the digest, or the wrong private key. |
| `SIGNATURE_INVALID` (wrong `v`)  | `v` was not normalized to the `27`/`28` range. Use `v = sig.v if sig.v >= 27 else sig.v + 27`.                                                                                                                                                                                                                |
| `SIGNATURE_INVALID` (wrong path) | A funds/permission action was signed via the Agent path (or vice-versa). Match the action to the correct path above.                                                                                                                                                                                          |

When you receive `SIGNATURE_INVALID`, the error message includes the recovered address and the expected address, which helps pinpoint which key you actually signed with.
