---
title: "How to sign a message or typed data"
description: "Plain messages, explicit encodings, and EIP-712 typed data."
---

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

# How to sign a message or typed data

Signing needs a token carrying the `sign` scope. Both routes address a **public
key directly** — you never walk account → wallet → address to sign.

## Sign a message

```bash
curl -X POST "$API_URL/sign/message" \
  -H "Authorization: Bearer $TOKEN" \
  -H "content-type: application/json" \
  -d '{"address":"0xabc…","message":"hello from tee-docker"}'
```

```json
{ "address": "0xabc…", "signature": "0x…" }
```

### Choosing an encoding

Omit `encoding` and the address signs with its VM's natural default. Name one
explicitly when the verifier expects a particular scheme:

| `encoding` | Use when |
|---|---|
| `personal_sign` | The verifier expects the EIP-191 personal-message prefix |
| `raw` | You are signing pre-hashed or otherwise pre-framed bytes |
| `ed25519` | Signing with an ed25519 key |

With an explicit encoding the response may also carry `messageHash`, so you can
check what was actually signed:

```json
{ "address": "0xabc…", "signature": "0x…", "messageHash": "0x…" }
```

> **raw signs exactly what you send**
>
> `raw` applies no prefix and no framing. A signature over attacker-chosen raw
> bytes can be replayed as something else entirely. Prefer `personal_sign` for
> anything human-originated.

## Sign typed data (EIP-712)

```bash
curl -X POST "$API_URL/sign/typed-data" \
  -H "Authorization: Bearer $TOKEN" \
  -H "content-type: application/json" \
  -d '{
"address": "0xabc…",
"typedData": {
  "domain": { "name": "MyDapp", "version": "1", "chainId": 1, "verifyingContract": "0xdef…" },
  "types": { "Order": [ { "name": "maker", "type": "address" }, { "name": "amount", "type": "uint256" } ] },
  "primaryType": "Order",
  "message": { "maker": "0xabc…", "amount": "1000" }
}
  }'
```

The response includes both intermediate hashes, so you can verify the domain
binding independently:

```json
{
  "address": "0xabc…",
  "signature": "0x…",
  "domainSeparator": "0x…",
  "structHash": "0x…"
}
```

### chainId is never guessed

`chainId` is optional. When omitted, the signature is bound to **the address's
own network**, not to a default chain.

> **Bind the chain deliberately**
>
> A signed payload is a bearer instrument. An earlier release defaulted a missing
> `chainId` to mainnet; that silent default was removed precisely because it let a
> testnet-intended signature carry mainnet weight. Pass `chainId` explicitly when
> the verifying contract lives on a specific chain.

Typed-data signing is EVM-only. Calling it with a non-EVM address returns `422`
with `unsupported_for_kind`.

## Failures worth handling

| Code | Status | What happened |
|---|---|---|
| `scope_denied` | 403 | Token lacks `sign` |
| `account_not_found` | 404 | No such address in this workspace |
| `account_locked` | 423 | The owning account is locked — its own password, an explicit lock, or a lapsed custody window. Unlock it |
| `unsupported_for_kind` | 422 | Typed data on a non-EVM address |
| `invalid_body` | 400 | Malformed request shape |

An address that exists but belongs to another tenant returns `account_not_found`
— identical to one that does not exist anywhere.

## Related

- [Scopes and permissions](/concepts/scopes/) — why `sign` is separate from `write`
- [Sign endpoints](/api/sign/) — exact shapes
- [Send transactions](/guides/send-transactions/) — signing that also submits

Source: https://tee.hypetrade.xyz/guides/sign-messages/index.mdx
