Submit signing
How to sign the message returned by GET /api/v1/executions/{walletAddress} and post it back to POST /api/v1/executions/{walletAddress}/submit, for all six signing standards: erc191 (EVM / MetaMask), nep413 (NEAR), raw_ed25519 (Solana / Phantom), sep53 (Stellar / Freighter), ton_connect (TON / Tonkeeper), and tip191 (Tron / TronLink).
TL;DR
Chain
signingStandard
What to sign
Signing op
Submit body fields
Signature encoding
EVM
erc191
payload.payload_json (string, verbatim)
personal_sign (EIP-191)
signature, executionId
secp256k1: + bs58(r‖s‖v0/1)
NEAR
nep413
payload.payload_json (parsed → {message,recipient,nonce})
NEP-413 signMessage
signature, publicKey, executionId
ed25519: + bs58(64 raw bytes)
Solana
raw_ed25519
decoded bytes of payload.payload_bytes_base64
signMessage(bytes)
signature, publicKey, executionId
ed25519: + bs58(64 raw bytes)
Stellar
sep53
payload.payload_json (string, verbatim)
Freighter signMessage (SEP-53)
signature, publicKey, executionId
ed25519: + bs58(64 raw bytes)
TON
ton_connect
payload.payload_json (string, verbatim, as TonConnect text)
TonConnect signData({type:'text'})
signature, publicKey, tonConnect, executionId
ed25519: + bs58(64 raw bytes)
Tron
tip191
payload.payload_json (string, verbatim)
signMessageV2 (TIP-191)
signature, executionId
secp256k1: + bs58(r‖s‖v0/1)
publicKey is required for NEAR/Solana/Stellar/TON and omitted for EVM and Tron (both secp256k1, signer recovered from the signature) — see below. TON additionally requires a tonConnect envelope — see the TON section.
The exact field to sign
Starting from the execution response:
GET /api/v1/executions/{walletAddress}
→ result.details.payload.{ payload_json, payload_bytes_base64, standard }A typical execution payload:
{
"result": {
"details": {
"payload": {
"payload_json": "{\"deadline\":\"2026-05-27T13:37:19Z\",\"intents\":[...],\"signer_id\":\"...\",\"verifying_contract\":\"intents.near\"}",
"payload_bytes_base64": "eyJkZWFkbGluZSI6...",
"standard": "erc191"
},
"signingStandard": "erc191"
}
}
}payload_json is the canonical source-of-truth field — always reach for result.details.payload.payload_json. payload_bytes_base64 is just the same JSON pre-encoded to the exact bytes the backend will verify against; it exists so Solana clients can sidestep UTF-8 surprises (see Solana section below).
Per chain:
erc191→ signpayload.payload_jsonverbatim as a string. Do notJSON.parseit and thenJSON.stringifyit — that can reorder keys or change whitespace and will change the hash. Do not pre-hash with keccak256;personal_signhashes for you.nep413→payload.payload_jsonmay be a JSON string or an already-parsed object; parse if it's a string, then feed{ message, recipient, nonce }to the NEAR wallet'ssignMessage. The wallet builds the NEP-413 prefix tag, borsh-encodes the payload, and hashes with SHA-256 internally — do not pre-hash, do not pre-borsh.raw_ed25519→ base64-decodepayload.payload_bytes_base64to aUint8Arrayand sign those bytes. Do not signpayload_jsondirectly here: Phantom'ssignMessageaccepts aUint8Array, and some wallet versions UTF-8 re-encode strings in a way that does not byte-match what the backend hashes. Do not wrap in any envelope (no SIWS, no"\x19Solana Signed Message:"), do not pre-hash.sep53→ signpayload.payload_jsonverbatim as a string (same field aserc191), passing it to Freighter'ssignMessage. Do not pre-hash and do not wrap it yourself — Freighter applies the SEP-53 framing ("Stellar Signed Message:\n"domain prefix + SHA-256) internally, and the backend'ssep53verifier expects exactly that.ton_connect→ signpayload.payload_jsonverbatim as the TonConnect text payload viasignData({ type: 'text', text }). Do not pre-hash and do not wrap it — TonConnect builds the0xffff || "ton-connect/sign-data/" … "txt" …+ SHA-256 digest internally. Unlike the others, you must also return the wallet-chosentonConnectenvelope ({domain, timestamp, address}) so the backend can rebuild that digest — see the TON section.tip191→ signpayload.payload_jsonverbatim as a string (same field aserc191), passing it to TronLink'ssignMessageV2. Do not pre-hash and do not wrap it yourself — the wallet applies the TIP-191 framing ("\x19TRON Signed Message:\n"prefix + keccak256) internally, the Tron analog ofpersonal_sign.
EVM / erc191
erc191Three encoding details that matter:
Argument order:
[message, address].vnormalization: only byte 64 of the 65-byte signature is touched (27 → 0,28 → 1). Do not editrors. Do not re-add27later.bs58, not base64: the NEAR intents verifier expects base58 with the
secp256k1:prefix.
Submit body:
Do not include publicKey. The backend runs ecrecover on the signature + the payload_json it has on file and compares the recovered address to the execution's signer_id (lowercase 0x… for erc191).
NEAR / nep413
nep413Four encoding details that matter:
Nonce is base64 →
Uint8Array. Don't pass the base64 string to the wallet. The wallet expects 32 raw bytes; if it receives the string, the borsh-encoded payload diverges from what the backend verifies.Signature re-encoding. NEP-413 wallets return base64. The backend verifier wants base58 with the
ed25519:prefix. Decode base64 → bytes → bs58 → prefix.Public key prefix. Wallets disagree about whether to include
ed25519:. Detect and add it if missing — never add it twice.No envelope on the message. Don't prepend
"NEAR Signed Message:"or anything similar. NEP-413's framing is what the wallet adds internally; doing it yourself double-wraps.
Submit body:
Three fields only. nonce, recipient, callbackUrl, state, accountId are not sent — the backend already has them from the execution it issued and only needs the signature material to verify.
Solana / raw_ed25519
raw_ed25519Three encoding details that matter:
Sign bytes, not a string.
signMessageaccepts aUint8Array; pass the decoded buffer directly.Signature is base58, not base64. Phantom returns 64 raw bytes (
R‖S). Encode withbs58, then prefix withed25519:.Public key: Phantom returns it in Solana's native base58 via
publicKey.toBase58(). Just prefix withed25519:— no further encoding. There is novbyte and no normalization step (that's an ERC-191 / secp256k1 thing).
Submit body:
The backend uses publicKey as both the verification key and — base58-decoded — as the signer_id it compares against the execution.
Stellar / sep53
sep53Four encoding details that matter:
Sign the
payload_jsonstring (likeerc191). Don't pre-hash and don't wrap it yourself — Freighter applies the SEP-53 framing ("Stellar Signed Message:\n"prefix + SHA-256) internally, and the backend'ssep53verifier expects exactly that.signedMessagemay be base64 or aUint8Array. Freighter versions disagree; normalize to a 64-byteR‖Sbuffer and assert the length — a different length means the wallet returned something other than a raw Ed25519 signature.Signature is base58, not base64. Encode the 64 bytes with
bs58, then prefix withed25519:(same as NEAR / Solana).Public key needs
StrKeydecoding first. The Stellar address is aG…StrKey string, not raw base58. Decode it withStrKey.decodeEd25519PublicKey()to 32 raw bytes, then bs58-encode and prefix withed25519:. Do not bs58 theG…string directly (unlike Solana, wherepublicKey.toBase58()is already the right base58).
Submit body:
Same three fields as NEAR/Solana. publicKey is required — Ed25519 cannot be recovered from the signature alone. {walletAddress} in the URL is the Stellar G… account from Freighter requestAccess().address.
Stellar deposit (memo required)
Separate from signing the /submit message: a Stellar deposit also needs a memo, and it must appear in two places. Stellar 1Click uses a shared deposit address, so the per-quote memo is the only thing that attributes the incoming funds to your quote — omit it on either step and the deposit is lost.
TON / ton_connect
ton_connectFour encoding details that matter:
Use
signData(text variant), notsendTransaction. This is an off-chain message signature, not an on-chain transaction. The wallet builds the0xffff || "ton-connect/sign-data/" … "txt" …+ SHA-256 digest internally — do not pre-hash, do not wrap the text.Signature is base64 from the wallet → base58 on the wire. TonConnect returns base64; decode to bytes, assert 64 (
R‖S),bs58-encode, then prefixed25519:(same as NEAR / Solana / Stellar).publicKeyis from connect time and mandatory. A TON address doesn't contain the key, so it can't be derived from the address or recovered from the signature. TonConnect exposesaccount.publicKey(hex) at connect;bs58-encode and prefixed25519:. Use a wallet that returns the account public key (e.g. Tonkeeper).The
tonConnectenvelope must be sent. The wallet foldsdomain,timestamp, andaddressinto the signed digest and picks them at signing time, so the backend rebuilds the digest from these wire values plus its stored copy of the payload. No other chain sends this.
Submit body:
publicKey and tonConnect are both required. Omitting the envelope returns 400 {"error": "tonConnect {domain, timestamp, address} is required for TON"}. {walletAddress} is the URL-safe non-bounceable account (UQ…). Full per-chain TON guide: ton-signing.md. Backend rationale: ../ton/ton-public-key.md and ../ton/ton-signature-verification.md.
Tron / tip191
tip191Three encoding details that matter:
Use
signMessageV2, not the legacysign/signMessage. Only V2 implements TIP-191 (the\x19TRON Signed Message:\nprefix the backend verifies); the older calls use a different, incompatible scheme.vnormalization: only byte 64 of the 65-byte signature is touched (27 → 0,28 → 1). Do not editrors. Identical toerc191.bs58, not base64: base58 with the
secp256k1:prefix.
Submit body:
Do not include publicKey. The backend recovers the secp256k1 signer, converts the wallet's base58 T… address to its embedded 20-byte account, and compares. Full per-chain guide: tron-signing.md.
Why publicKey is required for NEAR/Solana/Stellar/TON but not EVM/Tron
publicKey is required for NEAR/Solana/Stellar/TON but not EVM/TronEd25519 (NEP-413, raw_ed25519, sep53, and ton_connect) does not let the verifier recover the public key from the signature alone — the backend needs publicKey to verify and to derive the signer_id.
secp256k1 (erc191 and tip191) does: ecrecover reconstructs the address from the signature + message, so the field is omitted on the wire. Tron addresses embed the same 20-byte account as the recovered EVM address, so the recovered signer maps straight onto the base58 T… wallet.
TON is a further special case. Its address is hash(StateInit(code, pubkey)), so the key isn't even contained in the address (unlike a Solana base58 address or a Stellar G… StrKey, where the key can be read off the address). The frontend supplies the connect-time key, and the backend additionally proves that key owns the claimed wallet by re-deriving the address from it across every known wallet version — see ../ton/ton-public-key.md.
HTTP shape
{walletAddress} format per chain:
Solana — base58 (e.g.
7XSfQk…), the same stringwindow.solana.publicKey.toBase58()returns.NEAR — named account (
alice.near) or 64-char hex implicit account, the same string returned bywallet.getAccounts()[0].accountId.EVM —
0x…(40 hex chars, case-tolerant).Stellar —
G…StrKey ed25519 address, the same string FreighterrequestAccess()returns.TON — URL-safe non-bounceable user-friendly address (
UQ…), the stringAddress.parse(account.address).toString({ urlSafe: true, bounceable: false })returns. The bounceable (EQ…) and raw (<workchain>:<hex>) forms are also accepted — the backend matches on(workchain, accountHash), not the raw string.Tron — base58
T…address (e.g.TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t), the stringwindow.tronWeb.defaultAddress.base58returns.
Last updated
