Raw signing with Wallet-as-a-Service offers the ability to sign any transaction type available to a blockchain. It can be used on any transaction type if enabled.
This is an important feature of the Wallet-as-a-Service platform as it means customers can benefit from all natively supported transaction types of a blockchain as soon as they become available, regardless of whether that blockchain or transaction type is currently supported by Wallet-as-a-Service.
Raw signing can be enabled and disabled for a wallet from wallet settings.
Raw signing is a powerful and insecure signing method. It is therefore disabled by default. Please only enable raw signing for individual wallets if you fully understand this feature.
See our Wallet-as-a-Service API reference for information on how to submit raw transactions via the API.
For EVM wallets, the platform automatically detects the target blockchain from the chainId field in the encoded transaction and routes to the correct connector. You don't need to specify a blockchain parameter — the encoded transaction is the source of truth.
- Known chain ID — If the chain ID matches a natively integrated chain (Ethereum, Arbitrum, Polygon, Base, BNB Chain, Avalanche, 1Money), the platform routes the transaction to that chain's connector. The platform supports both
signOnly=trueandsignOnly=false. - Unknown chain ID — If the chain ID doesn't match any natively integrated chain, the platform treats the transaction as cross-chain: it signs only and requires a policy with an explicit
CHAIN_IDmatcher.
1Money wallets are an exception: raw transactions from a 1Money wallet always execute on 1Money, regardless of the chain ID in the encoded transaction.
The blockchain field on the raw transaction request is deprecated. The platform ignores it for all raw transactions. For EVM wallets, the chain ID in the encoded transaction determines routing — a contradicting blockchain value is ignored with no error or warning. Raw transactions from non-EVM wallets and 1Money wallets always execute on the wallet's native blockchain.
An Ethereum wallet can sign and broadcast an Arbitrum transaction without any override:
POST /v2/vaults/{vaultId}/wallets/{walletId}/transactions/raw
{
"encodedTransaction": "<RLP-encoded transaction with Arbitrum chainId>",
"signOnly": false
}The platform extracts the Arbitrum chain ID from the encoded transaction, routes to the Arbitrum connector, and broadcasts the transaction.
The platform doesn't check the wallet's balance on the target chain before broadcasting. Your address is the same on every EVM chain, but it needs the native token for gas on the target chain. If the address isn't funded there, the network rejects the transaction and it fails with an insufficient funds reason.
For EVM wallets, you can raw-sign transactions for chain IDs that Wallet-as-a-Service doesn't natively integrate — for example, custom L2s, testnets, or chains Wallet-as-a-Service hasn't yet onboarded.
Cross-chain raw signing uses the same raw transaction endpoint. When the platform detects a chain ID that doesn't match any natively integrated chain, it enforces sign-only, checks a chain-ID-scoped policy, and returns the signature. Wallet-as-a-Service doesn't broadcast the transaction — you submit the signed transaction to the target chain yourself.
- EVM-only — Available for EVM wallets only.
- Sign-only — The request must set
signOnly=true. Wallet-as-a-Service rejects requests withsignOnly=falsefor an unknown chain ID with HTTP 400. - Explicit
CHAIN_IDmatcher required — The wallet must have a policy whose matchers include aCHAIN_IDmatcher for the target chain ID. Base policies without matchers don't apply to cross-chain transactions — a base policy alone doesn't authorize the sign. - Natively integrated chain IDs rejected — Wallet-as-a-Service rejects a
CHAIN_IDmatcher value that matches a chain it integrates natively (for example, Ethereum mainnet1). Use the standard raw signing flow for those chains.
- Create a policy on the EVM wallet that includes a
CHAIN_IDmatcher for each chain ID you want to allow. See Policy reference for the matcher format. - Submit a raw transaction to the standard raw signing endpoint with
signOnly=true. Encode the transaction using the EVM format described in Transaction encoding formats, setting thechainIdfield to the target chain ID. - The platform detects the unknown chain ID, evaluates the policy, and returns the signature on success.
- Submit the signed transaction to the target chain yourself.
Cross-chain raw signing returns a signature only. Wallet-as-a-Service has no connector for the target chain and doesn't publish the transaction. You're responsible for broadcasting the transaction — and for funding the address with the target chain's native gas token when you submit it.
When using the Raw Transaction API, the encodedTransaction field must be encoded in a blockchain-specific format. This section details the exact encoding requirements for each supported blockchain.
The encodedTransaction field must match the output of go-ethereum's rlp.EncodeToBytes(types.Transaction).
Format:
RLP(type_byte || RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gas, to, value, data, accessList, v, r, s]))Key requirements:
- Include v, r, s signature fields — Set to
0for unsigned transactions - Apply outer RLP wrapper — The typed transaction must be wrapped as an RLP byte string
If you receive "typed transaction too short", you're likely using the standard unsigned format (0x02 || RLP([9 fields])) which is missing the v/r/s placeholders and outer RLP wrapper.
Python example:
from eth_account.typed_transactions import DynamicFeeTransaction
import rlp
# Use the signed transaction serializer (includes v, r, s fields)
serializer = DynamicFeeTransaction._signed_transaction_serializer
tx = serializer(
chainId=84532, # Base Sepolia
nonce=nonce,
maxPriorityFeePerGas=max_priority,
maxFeePerGas=max_fee,
gas=gas_limit,
to=bytes.fromhex(to_address[2:]),
value=0,
data=calldata_bytes,
accessList=(),
v=0, # Placeholder for unsigned
r=0, # Placeholder for unsigned
s=0, # Placeholder for unsigned
)
# RLP encode the fields
tx_rlp = rlp.encode(tx)
# Add EIP-1559 type prefix (0x02)
typed_tx = bytes([2]) + tx_rlp
# Wrap in RLP string (matches go-ethereum's rlp.EncodeToBytes)
encoded_transaction = rlp.encode(typed_tx).hex()Go example:
import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
)
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: big.NewInt(84532),
Nonce: nonce,
GasTipCap: maxPriorityFeePerGas,
GasFeeCap: maxFeePerGas,
Gas: gasLimit,
To: &toAddress,
Value: big.NewInt(0),
Data: calldata,
})
encodedBytes, _ := rlp.EncodeToBytes(tx)
encodedTransaction := hex.EncodeToString(encodedBytes)The encodedTransaction field must be an XRP Binary Codec signing payload: the output of encodeForSigning for single-signing, or encodeForMultisigning for multi-signing. Only these payloads produce valid signatures — the endpoint doesn't check for a signing prefix and signs plain encode output verbatim.
Format: Hex-encoded binary codec signing payload of the transaction JSON
Key requirements:
- For single-signed transactions, set
SigningPubKeyto the wallet's public key - Use
encodeForSigning(notencode) for unsigned transactions - For multi-signed transactions, use
encodeForMultisigningwith the signer's address, leaveSigningPubKeyempty, and setsignOnly: true
Wallet-as-a-Service (Palisade) signs exactly the bytes you submit. XRPL verifies each signature against a signing payload that starts with a 4-byte prefix: encodeForSigning prepends 53545800 (STX\0) for single-signing, and encodeForMultisigning prepends 534D5400 (SMT\0) for multi-signing. Plain encode adds neither. If you submit encode output, the request returns HTTP 200 and the transaction reaches SIGNED, but XRPL rejects the result with Invalid signature: with signOnly: false, the transaction ends in FAILED; with signOnly: true, you receive a signature that XRPL rejects.
Quick check: confirm that your encodedTransaction hex starts with 53545800 (single-sign) or 534D5400 (multi-sign).
Multi-signing: Build the transaction's Signers array from the returned canonicalSignature. The platform assembles single-signed blobs only, so for a 534D5400 payload, ignore the returned signedTransaction and hash.
JavaScript example:
const { encodeForSigning } = require('ripple-binary-codec');
const tx = {
Account: "rSourceAddress...",
Destination: "rDestAddress...",
Amount: "1000000", // In drops
Fee: "10",
Sequence: 12345,
SigningPubKey: "02ECE63017B0FEFC...", // Required
TransactionType: "Payment"
};
const encodedTransaction = encodeForSigning(tx);
// For multi-signed transactions, set SigningPubKey to "" and pass the signer's address:
// const encodedTransaction = encodeForMultisigning({ ...tx, SigningPubKey: "" }, signerAddress);Python example:
from xrpl.core.binarycodec import encode_for_signing
tx = {
"Account": "rSourceAddress...",
"Destination": "rDestAddress...",
"Amount": "1000000",
"Fee": "10",
"Sequence": 12345,
"SigningPubKey": "02ECE63017B0FEFC...",
"TransactionType": "Payment"
}
encoded_transaction = encode_for_signing(tx)
# For multi-signed transactions, set SigningPubKey to "" and pass the signer's address:
# encoded_transaction = encode_for_multisigning({**tx, "SigningPubKey": ""}, signer_address)Java example (xrpl4j):
import org.xrpl.xrpl4j.codec.binary.XrplBinaryCodec;
XrplBinaryCodec codec = XrplBinaryCodec.getInstance();
String txJson = """
{
"Account": "rSourceAddress...",
"Destination": "rDestAddress...",
"Amount": "1000000",
"Fee": "10",
"Sequence": 12345,
"SigningPubKey": "02ECE63017B0FEFC...",
"TransactionType": "Payment"
}
""";
// Single-signing: prepends 53545800
String encodedTransaction = codec.encodeForSigning(txJson);
// For multi-signed transactions, pass the signer's address
// (and leave SigningPubKey empty in the transaction JSON):
// String encodedTransaction = codec.encodeForMultiSigning(txJson, signerAddress);The encodedTransaction field must be a base64-encoded serialized Solana transaction.
Format: base64(transaction.MarshalBinary())
Key requirements:
- Transaction must include a valid recent blockhash
- Account keys must be properly ordered (fee payer first)
- Use standard base64 encoding (not base58)
Python example:
from solders.transaction import Transaction
from solders.message import Message
import base64
# Build your transaction message
message = Message.new_with_blockhash(
instructions,
payer,
blockhash
)
# Create unsigned transaction
tx = Transaction.new_unsigned(message)
# Serialize and base64 encode
tx_bytes = bytes(tx)
encoded_transaction = base64.b64encode(tx_bytes).decode('utf-8')JavaScript example:
const { Transaction } = require('@solana/web3.js');
const tx = new Transaction();
tx.recentBlockhash = blockhash;
tx.feePayer = payerPublicKey;
tx.add(instruction);
// Serialize (without signing)
const serialized = tx.serialize({ requireAllSignatures: false });
const encodedTransaction = serialized.toString('base64');The encodedTransaction field must be a hex-encoded TRON Transaction protobuf.
Format: hex(proto.Marshal(Transaction))
Key requirements:
- Transaction must include valid
ref_block_bytesandref_block_hashfrom a recent block - Expiration timestamp must be in the future (typically current time + 60 seconds, in milliseconds)
- Fee limit must be set for TRC-20 transfers (recommended: 15,000,000 SUN = 15 TRX)
- The signing hash is
SHA256(raw_data)— Wallet-as-a-Service computes this automatically
Supported contract types:
| Contract Type | Description |
|---|---|
TransferContract | Native TRX transfers |
TriggerSmartContract | TRC-20 token transfers and smart contract calls |
Python example (using tronpy):
from tronpy import Tron
client = Tron(network='shasta') # or 'mainnet'
# Build a TRX transfer transaction
txn = (
client.trx.transfer(
from_="TSourceAddress...",
to="TDestAddress...",
amount=1_000_000 # 1 TRX in SUN
)
.fee_limit(1_000_000) # 1 TRX fee limit
.build()
)
# Get the raw transaction bytes (protobuf serialized)
raw_bytes = txn._raw_data.SerializeToString()
# Hex encode for Wallet-as-a-Service
encoded_transaction = raw_bytes.hex()
# For TRC-20 transfers, use trigger_smart_contract insteadJavaScript example (using TronWeb):
const TronWeb = require('tronweb');
const tronWeb = new TronWeb({
fullHost: 'https://api.shasta.trongrid.io', // or mainnet
});
// Build a TRX transfer transaction
const tx = await tronWeb.transactionBuilder.sendTrx(
'TDestAddress...', // to
1000000, // amount in SUN (1 TRX)
'TSourceAddress...' // from
);
// The transaction object contains raw_data_hex
const encodedTransaction = tx.raw_data_hex;Go example:
import (
"encoding/hex"
"time"
"github.com/fbsobreira/gotron-sdk/pkg/proto/core"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)
// Build a TRX transfer
transfer := &core.TransferContract{
OwnerAddress: ownerAddrBytes, // 21-byte TRON address
ToAddress: toAddrBytes,
Amount: 1000000, // 1 TRX in SUN
}
anyValue, _ := anypb.New(transfer)
tx := &core.Transaction{
RawData: &core.TransactionRaw{
Contract: []*core.Transaction_Contract{{
Type: core.Transaction_Contract_TransferContract,
Parameter: anyValue,
}},
RefBlockBytes: refBlockBytes, // From recent block
RefBlockHash: refBlockHash,
Expiration: time.Now().Add(60*time.Second).UnixMilli(),
Timestamp: time.Now().UnixMilli(),
},
}
txBytes, _ := proto.Marshal(tx)
encodedTransaction := hex.EncodeToString(txBytes)TRON addresses can be in base58 format (starts with T) or hex format (starts with 41). The API accepts base58 addresses, but internally they are converted to 21-byte hex addresses in the protobuf.
TRON transactions require ref_block_bytes and ref_block_hash from a recent block (within ~18 hours). If you're building transactions manually, fetch the latest block and extract bytes 6-8 of the block number and bytes 8-16 of the block hash.
The encodedTransaction field must be a hex-encoded JSON object with a transactionType and a payload.
Format: hex(JSON({ transactionType, payload }))
{
"transactionType": "TokenMint",
"payload": {
"chain_id": 1212101,
"nonce": 0,
"recipient": "0x...",
"value": 1000000000000000000,
"token": "0x..."
}
}Key requirements:
- Set
transactionTypeto one of the supported transaction types. (See 1Money transaction types.) - Include the 1Money
chain_idin every payload - Set
nonceto the wallet's current sequence, fromGET /v2/vaults/{vaultId}/wallets/{walletId}/sequence. (See Example: mint tokens.) - Use snake_case payload field names, as shown in the examples.
The envelope is the same for every 1Money transaction, but the payload fields differ. Check 1Money transaction types before encoding — several types share a shape, and a few (notably BatchPayment) don't.
1Money is a stablecoin-native chain, so its tokens don't follow ERC-20 or SPL. Token operations are expressed as explicit transaction types rather than contract calls, and you access them through raw signing.
Encode each of the payloads below using the envelope described in 1Money under Transaction encoding formats.
| Transaction type | Description |
|---|---|
TokenCreate | Issue a new token |
TokenMint | Mint tokens to an address |
TokenBurn | Burn tokens from an address |
TokenTransfer | Transfer tokens between addresses |
TokenGrantAuthority | Grant an authority role to an address |
TokenRevokeAuthority | Revoke an authority role from an address |
TokenBlacklistAccount | Add an address to the token blacklist |
TokenWhitelistAccount | Add an address to the token whitelist |
TokenPause | Pause token operations |
TokenUnpause | Unpause token operations |
TokenUpdateMetadata | Update the token name, URI, or metadata |
BatchPayment | Pay many recipients of one token in a single transaction |
These three types share the same payload shape:
{
"transactionType": "TokenMint",
"payload": {
"chain_id": 1212101,
"nonce": 0,
"recipient": "0x1234567890123456789012345678901234567890",
"value": 1000000000000000000,
"token": "0xabcdef0123456789abcdef0123456789abcdef01"
}
}{
"transactionType": "TokenCreate",
"payload": {
"chain_id": 1212101,
"nonce": 0,
"symbol": "MYTOKEN",
"name": "My Token",
"decimals": 18,
"master_authority": "0x1234567890123456789012345678901234567890",
"is_private": false
}
}The response metadata for a TokenCreate transaction includes the new token's contract address:
{
"transaction_identifier": {
"hash": "0x..."
},
"metadata": {
"token_contract_address": "0x1234567890123456789012345678901234567890"
}
}{
"transactionType": "TokenGrantAuthority",
"payload": {
"chain_id": 1212101,
"nonce": 0,
"action": "Grant",
"authority_type": "MasterMintBurn",
"authority_address": "0x1234567890123456789012345678901234567890",
"token": "0xabcdef0123456789abcdef0123456789abcdef01",
"value": 0
}
}Authority types: MasterMintBurn, MintBurn, Pause, List, MetadataUpdate, BridgeMint.
{
"transactionType": "TokenBlacklistAccount",
"payload": {
"chain_id": 1212101,
"nonce": 0,
"action": "Add",
"address": "0x1234567890123456789012345678901234567890",
"token": "0xabcdef0123456789abcdef0123456789abcdef01"
}
}{
"transactionType": "TokenPause",
"payload": {
"chain_id": 1212101,
"nonce": 0,
"action": "Pause",
"token": "0xabcdef0123456789abcdef0123456789abcdef01"
}
}{
"transactionType": "TokenUpdateMetadata",
"payload": {
"chain_id": 1212101,
"nonce": 0,
"name": "Updated Token Name",
"uri": "https://example.com/token-metadata",
"token": "0xabcdef0123456789abcdef0123456789abcdef01",
"additional_metadata": []
}
}BatchPayment pays many recipients of a single token in one transaction:
{
"transactionType": "BatchPayment",
"payload": {
"chain_id": 1212101,
"nonce": 0,
"token": "0xabcdef0123456789abcdef0123456789abcdef01",
"operations": [
{ "recipient": "0x1234567890123456789012345678901234567890", "amount": 1500000 },
{ "recipient": "0xa0B640D215622e72198D1BFf88aCd8aFB9198844", "amount": 2500000 }
],
"created_at": 1755090000
}
}Key behaviors:
operationsis ordered, and the order is significant — it forms part of the signed bytes.created_atis caller-supplied Unix seconds — the SDK doesn't stamp a clock of its own. It also forms part of the signed bytes, so use the same value throughout signing and submission.operations_hashandbatch_idare optional trailing fields. Leavingoperations_hashunset is always valid. If you set it, it must equal the canonical hash ofoperations(useDeriveBatchPaymentOperationsHash) or the node rejects the transaction.- Policies evaluate the sum of the operation amounts, not a per-recipient figure, so a transaction limit must cover the whole batch. Each token contract needs its own configured limit — assets are keyed on symbol, blockchain, and contract address.
- These payloads fail locally before signing rather than at the node: an empty
operationslist, a zero-address recipient, a zero or absentamount, amounts totalling more than 2^256−1, and anoperations_hashthat doesn't matchoperations.
Unlike the other transaction types, BatchPayment exists only in the SDK's domain-separated v2 format and has no legacy v1 equivalent. The connector handles this — you don't need to do anything differently — but a client configured with the 1Money SDK's WithLegacyV1 option can't send one.
Get the current nonce for your wallet:
GET /v2/vaults/{vaultId}/wallets/{walletId}/sequence{ "sequence": "5" }Build the raw transaction in your application:
payload := onemoney.TokenMintPayload{ ChainID: 1212101, Nonce: 5, // from step 1 Recipient: common.HexToAddress("0xa0B640D215622e72198D1BFf88aCd8aFB9198844"), Value: big.NewInt(1000000000000000000), // 1 token (18 decimals) Token: common.HexToAddress("0x6b01a50120ebcd864a0376375d911a001395a062"), } rawTx := map[string]interface{}{ "transactionType": "TokenMint", "payload": payload, } // Marshal to JSON and hex encode jsonBytes, _ := json.Marshal(rawTx) encodedTx := hex.EncodeToString(jsonBytes)Submit the raw transaction:
POST /v2/vaults/{vaultId}/wallets/{walletId}/transactions/raw Content-Type: application/json { "encodedTransaction": "7b227472616e73616374696f6e54797065223a22546f6b656e4d696e74222c...", "signOnly": false }{ "id": "tx-uuid-here", "status": "PENDING_APPROVAL", "asset": { "id": "asset-uuid", "symbol": "BOB" } }Poll the transaction status until it's confirmed. See Manage transactions.
GET /v2/vaults/{vaultId}/wallets/{walletId}/transactions/{txId}