Skip to content

How to sign messages, typed data, and raw payloads

Signing with an address rather than an account — messages, EIP-712 typed data, raw EVM digests, and raw SVM bytes — choosing an encoding, and the scope you need first.

Updated View as Markdown

Examples on this page use $API_URL. Set it once, with the /v1 prefix included — every route lives under it:

export API_URL=https://<your-api-host>/v1

Signing needs a token carrying the sign scope. All four signing routes address a public key directly — you never walk account → wallet → address to sign.

Sign a message

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

Choosing an encoding

Omit encoding and the address signs with its VM’s natural default — which works for every address kind. Name one explicitly when the verifier expects a particular scheme, keeping in mind that each explicit encoding belongs to one address kind:

encoding Works on Use when
personal_sign EVM only The verifier expects the EIP-191 personal-message prefix
raw EVM only The verifier expects a keccak-256 hash with no EIP-191 prefix
ed25519 Solana only Signing with an ed25519 key

Asking for an encoding the address cannot perform returns 501 unsupported_operation.

message is always read as UTF-8 text, whatever the encoding. The string "0xdeadbeef" is signed as those ten characters, not as the four bytes they spell — the one exception is raw, which decodes its input as hex and therefore requires 0x-prefixed hex of even length.

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

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

For personal_sign and raw that is the digest actually signed. For ed25519 it is the message bytes as hex — ed25519 signs the message directly rather than a prehash, so there is no digest to report.

Sign typed data (EIP-712)

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:

{
  "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.

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

Three more things worth knowing before you debug a payload that will not sign. Send typedData as an object — eth_signTypedData_v4 carries it already stringified in its second RPC parameter, and forwarding that string is rejected. Every address inside the payload must be all-lowercase, all-uppercase, or correctly EIP-55 checksummed; a mixed-case address with a bad checksum is rejected, including the example verifyingContract printed in the EIP-712 specification itself. And if you forward a types.EIP712Domain list, keep it consistent with your domain — the service does not cross-check the two, but that list is what the signing engine encodes the domain from, so a mismatch changes or breaks the result. The Sign reference has the details.

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
unsupported_operation 501 An encoding this address kind cannot perform
invalid_body 400 Malformed request shape. The message names the fields that failed
chain_id_mismatch 400 domain.chainId disagrees with the signing chain
invalid_parameter 400 The signing engine rejected the payload contents — a bad field type, or a failed address checksum

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

Navigation

Type to search…

↑↓ navigate↵ selectEsc close