EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8050: Compressed RPC Link Format

A compact, URL-safe format for wallet RPC requests using Protocol Buffers and Base64url encoding.

Authors
Created2025-10-13
Discussion Linkhttps://ethereum-magicians.org/t/erc-8050-compressed-rpc-link-format-with-method-specific-shortcuts/25832
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1261PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationPayload FormatCompression FlagsCore Protocol Buffers SchemaVersioningShortcut RegistryShortcut 0: Generic JSON-RPCSchemaEncoding RulesDecoding AlgorithmExampleShortcut 1: walletsendCallsSchemaType Detection (Normative)Encoding AlgorithmDecoding AlgorithmExample: ERC-20 TransferShortcut 2: walletsignSchemaType Detection (Normative)Encoding AlgorithmDecoding AlgorithmExample: SpendPermissionCanonical Encodings (Normative)AddressesInteger AmountsFixed-Size FieldsCalldataChain IDStringsCapabilitiesComplete Encoding AlgorithmComplete Decoding AlgorithmError Handling (Normative)Capability ExtensionsData Callback Capability (ERC-8026)Security requirementsSize Savings Analysis (Informative)RationaleProtocol BuffersBrotli CompressionBase64urlShortcut ArchitectureNo Hardcoded EnumsMinimal Core SchemaBackwards CompatibilitySecurity ConsiderationsRequest ValidationPrivacyFront-Running and MEVResource LimitsCallback SecurityTest VectorsTest Vector 1: ERC-20 Transfer (Shortcut 1)Test Vector 2: Native Transfer (Shortcut 1)Test Vector 3: SpendPermission (Shortcut 2)Copyright

Abstract

This ERC defines a compact, URL-safe payload format for JSON-RPC requests targeting wallet interactions. It uses Protocol Buffers for binary serialization, an optional Brotli compression layer, and Base64url encoding for transport. The format supports shortcuts: method-specific encodings that optimize size and structure for particular RPC methods. This ERC standardizes three shortcuts:

  • Shortcut 0: Generic JSON-RPC (universal fallback for any method)
  • Shortcut 1: wallet_sendCalls (EIP-5792) with optimized encodings for ERC-20 transfers, native transfers, and generic calls
  • Shortcut 2: wallet_sign (EIP-7871) with optimized encodings for spend permissions and receive-with-authorization signatures

Motivation

Applications often need to pass wallet RPC requests through QR codes, NFC tags, or deep links. JSON is verbose and not URL-friendly at small sizes. This standard provides:

  • Universal compatibility: Any JSON-RPC request can be encoded via the generic shortcut
  • Optimization: Common transaction patterns achieve 60-80% size reduction via specialized shortcuts
  • Interoperability: A single format works across apps, wallets, and programming languages
  • Extensibility: New shortcuts can be added without changing the core format

This enables one-step, connection-agnostic flows for payments, signatures, and other wallet interactions.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Payload Format

Payloads are Base64url-encoded (RFC 4648) binary data without padding. The entire string is the payload; applications MAY wrap it in URLs.

{base64url_encoded_payload}

ABNF (normative):

payload = 1*( ALPHA / DIGIT / "-" / "_" )

URL embedding (informative):

  • Recommended query parameter: p
  • Recommended link form: {any-https-url}?p={payload}
  • Implementations SHOULD use iOS Universal Links and Android App Links for routing
  • Wallets MUST accept raw payloads (e.g., from QR/NFC) and MAY accept URLs carrying p

Encoding requirements:

  • Encoders MUST NOT include Base64url padding (=)
  • Decoders MUST accept payloads with or without padding
  • Decoders MUST reject:
    • Characters outside the Base64url alphabet (RFC 4648): A-Z, a-z, 0-9, -, _
    • Invalid padding (padding characters not at the end, or incorrect padding length)
    • Payloads that cannot be decoded to valid binary data

Compression Flags

The first byte of the binary payload indicates the compression method:

  • 0x00: No compression (Protocol Buffers only)
  • 0x01: Brotli compressed

Values 0x02–0xFF are reserved. Decoders MUST reject unknown values.

Conformance:

  • Decoders MUST implement Brotli decompression for 0x01 with support for:
    • Quality levels 0-11
    • Window size up to 24 bits (16 MB)
    • Standard Brotli RFC 7932 format
  • Encoders MAY emit 0x00 when compression is not beneficial
  • Encoders SHOULD use 0x01 if the compressed size (including the flag byte) is strictly smaller than uncompressed

Recommended encoder settings (informative):

  • Quality: 4-6 (balances speed and compression ratio)
  • Window size: 22 bits (4 MB) - sufficient for typical payloads
  • Mode: GENERIC (works well for mixed text/binary data)
  • These settings provide good compression while maintaining fast encoding/decoding

Core Protocol Buffers Schema

Implementations MUST use the following Protocol Buffers v3 schema for the core payload:

syntax = "proto3";

message RpcLinkPayload {
  uint32 protocol_version = 1;   // Core payload version. This ERC defines version 1.

  // Chain context (optional at core level; shortcuts MAY require it)
  uint32 chain_id = 2;             // Canonical numeric chain ID (e.g., 1, 8453, 84532)

  // Shortcut selection and versioning
  uint32 shortcut_id = 3;          // 0 = GENERIC_JSON_RPC, 1 = WALLET_SEND_CALLS, 2 = WALLET_SIGN
  uint32 shortcut_version = 4;     // Shortcut-specific version (0 for shortcuts defined in this ERC)

  // Payload (selected by shortcut_id)
  oneof body {
    GenericJsonRpc generic = 10;         // shortcut_id = 0
    WalletSendCalls wallet_send_calls = 11;  // shortcut_id = 1
    WalletSign wallet_sign = 12;         // shortcut_id = 2
  }

  // Extension point for metadata/capabilities (values are UTF-8 JSON)
  map<string, bytes> capabilities = 20;
}

Protocol Buffers version (normative):

  • Syntax: proto3
  • Wire format: Protocol Buffers v3 (compatible with Protobuf 3.21+)
  • Unknown fields: MUST be ignored per proto3 semantics
  • Unknown enum discriminants: MUST be rejected unless explicitly allowed

Versioning

  • protocol_version defines the core wire format (this ERC defines version 1)
  • shortcut_id selects the shortcut; shortcut_version is managed by the shortcut definition
  • Backward compatibility: fields MUST NOT be repurposed within the same protocol version
  • Forward compatibility: decoders MUST ignore unknown fields and reject unknown enum discriminants

Decoder support policy:

  • Decoders MUST accept protocol_version = 1
  • Decoders MUST reject unsupported protocol_version with an explicit error
  • If shortcut_id is unsupported, decoders MUST return an error indicating unsupported shortcut

Shortcut Registry

  • 0 – GENERIC_JSON_RPC (this ERC; universal fallback)
  • 1 – WALLET_SEND_CALLS (this ERC; optimizes EIP-5792 wallet_sendCalls)
  • 2 – WALLET_SIGN (this ERC; optimizes EIP-7871 wallet_sign)
  • 3..99 – Reserved for future standardized shortcuts
  • 1000..1999 – Experimental/vendor shortcuts (MAY be used in private ecosystems)

Wallets and apps MAY support any subset of shortcuts. Unsupported shortcuts MUST trigger an explicit error.

Shortcut 0: Generic JSON-RPC

Purpose: Universal fallback for any JSON-RPC method.

Schema

message GenericJsonRpc {
  string method = 1;        // JSON-RPC method name (e.g., "eth_sendTransaction", "wallet_sendCalls")
  bytes params_json = 2;    // UTF-8 JSON-encoded params array or object (RFC 8259)
  string rpc_version = 3;   // Optional JSON-RPC version (e.g., "2.0")
}

Encoding Rules

  • shortcut_id = 0
  • method: case-sensitive, MUST match the target RPC interface
  • params_json: MUST be well-formed UTF-8 JSON (array or object per JSON-RPC spec)
  • rpc_version: OPTIONAL; decoders MUST NOT rely on it
  • chain_id: MAY be present; generic shortcut does not require it

Decoding Algorithm

  1. Validate shortcut_id = 0

  2. Parse method and params_json (UTF-8 decode, then JSON parse)

  3. Reconstruct JSON-RPC request:

    {
      "jsonrpc": rpc_version || "2.0",
      "method": method,
      "params": JSON.parse(params_json)
    }

Example

Input:

{
  "method": "eth_sendTransaction",
  "params": [{
    "from": "0x1111111111111111111111111111111111111111",
    "to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
    "value": "0x0",
    "data": "0xa9059cbb..."
  }]
}

Encoding (pseudocode):

const payload: RpcLinkPayload = {
  protocolVersion: 1,
  chainId: 0, // optional
  shortcutId: 0,
  shortcutVersion: 0,
  body: {
    case: 'generic',
    value: {
      method: 'eth_sendTransaction',
      paramsJson: utf8Encode(JSON.stringify([{ from: '0x11...', to: '0xa0...', ... }])),
      rpcVersion: '2.0'
    }
  }
};

Shortcut 1: wallet_sendCalls

Purpose: Optimized encoding for EIP-5792 wallet_sendCalls requests.

Schema

message WalletSendCalls {
  // Transaction type discriminator
  SendCallsType type = 1;

  // Type-specific data
  oneof transaction_data {
    Erc20Transfer erc20_transfer = 10;
    NativeTransfer native_transfer = 11;
    GenericCalls generic_calls = 12;
  }

  // Optional sender address (from field)
  bytes from = 3;  // 20-byte address

  // RPC version (e.g., "1.0")
  string version = 4;
}

enum SendCallsType {
  SEND_CALLS_UNKNOWN = 0;
  ERC20_TRANSFER = 1;
  NATIVE_TRANSFER = 2;
  GENERIC_CALLS = 3;
}

message Erc20Transfer {
  bytes token = 1;      // 20-byte ERC20 token contract address
  bytes recipient = 2;  // 20-byte recipient address
  bytes amount = 3;     // Amount in token's smallest unit (big-endian bytes, minimal encoding)
}

message NativeTransfer {
  bytes recipient = 1;  // 20-byte recipient address
  bytes amount = 2;     // Amount in wei (big-endian bytes, minimal encoding)
}

message Call {
  bytes to = 1;         // 20-byte contract/EOA address
  bytes data = 2;       // Calldata (may be empty)
  bytes value = 3;      // Value in wei (big-endian bytes, minimal encoding)
                        // Note: For zero value, encoders MAY omit field or use 0x00
}

message GenericCalls {
  repeated Call calls = 1;
}

Type Detection (Normative)

Encoders MUST detect transaction types in this order:

  1. ERC-20 Transfer: Exactly one call where:

    • data starts with 0xa9059cbb (ERC-20 transfer(address,uint256) selector)
    • value is 0x0, empty, or omitted
    • data length is exactly 68 bytes (4-byte selector + 32-byte padded address + 32-byte padded amount)
    • Note: 68 bytes = 136 hex characters (after removing 0x prefix)
  2. Native Transfer: Exactly one call where:

    • data is empty or 0x
    • value is non-zero
  3. Generic Calls: Any other wallet_sendCalls request

Encoding Algorithm

  1. Parse EIP-5792 wallet_sendCalls request
  2. Detect transaction type (see above)
  3. Extract fields:
    • ERC-20: Extract token (to address), recipient (bytes 4-35 of data), amount (bytes 36-67 of data, decoded as uint256)
    • Native: Extract recipient (to address), amount (value field)
    • Generic: Preserve all calls as-is
  4. Set from if present in original request
  5. Set version if present (e.g., "1.0")
  6. Populate RpcLinkPayload with shortcut_id = 1, chain_id, and capabilities

Decoding Algorithm

Helper function pad32: Left-pad bytes with zeros to exactly 32 bytes. For addresses (20 bytes), prepend 12 zero bytes. For amounts, convert to big-endian 32-byte representation.

  1. Validate shortcut_id = 1

  2. Switch on type:

    • ERC-20: Reconstruct call with:
      • to = token (as hex with 0x prefix)
      • data = 0xa9059cbb + pad32(recipient) + pad32(amount) (concatenate selector + padded params)
      • value = 0x0
    • Native: Reconstruct call with:
      • to = recipient (as hex with 0x prefix)
      • data = 0x (empty)
      • value = amount (as hex with 0x prefix)
    • Generic: Use calls as-is
  3. Reconstruct EIP-5792 request:

    {
      "method": "wallet_sendCalls",
      "params": [{
        "version": version || "1.0",
        "chainId": "0x" + chainId.toString(16),
        "from": from ? "0x" + from.hex() : undefined,
        "calls": [...],
        "capabilities": parseCapabilities(capabilities)
      }]
    }

Example: ERC-20 Transfer

Input (EIP-5792):

{
  "method": "wallet_sendCalls",
  "params": [{
    "version": "1.0",
    "chainId": "0x2105",
    "calls": [{
      "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "data": "0xa9059cbb000000000000000000000000fe21034794a5a574b94fe4fdfd16e005f1c96e5100000000000000000000000000000000000000000000000000000000004c4b40",
      "value": "0x0"
    }]
  }]
}

Encoded (pseudocode):

{
  protocolVersion: 1,
  chainId: 8453,
  shortcutId: 1,
  shortcutVersion: 0,
  body: {
    case: 'walletSendCalls',
    value: {
      type: SendCallsType.ERC20_TRANSFER,
      transactionData: {
        case: 'erc20Transfer',
        value: {
          token: hex('833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'),
          recipient: hex('fe21034794a5a574b94fe4fdfd16e005f1c96e51'),
          amount: bigIntToBytes(5000000n) // 0x4c4b40
        }
      },
      version: '1.0'
    }
  }
}

Shortcut 2: wallet_sign

Purpose: Optimized encoding for EIP-7871 wallet_sign requests with EIP-712 typed data.

Schema

message WalletSign {
  // Signature type discriminator
  SignType type = 1;

  // Type-specific data
  oneof signature_data {
    SpendPermission spend_permission = 10;
    ReceiveWithAuthorization receive_with_authorization = 11;
    GenericTypedData generic_typed_data = 12;
  }

  // RPC version (e.g., "1")
  string version = 3;
}

enum SignType {
  SIGN_UNKNOWN = 0;
  SPEND_PERMISSION = 1;
  RECEIVE_WITH_AUTHORIZATION = 2;
  GENERIC_TYPED_DATA = 3;
}

message SpendPermission {
  // EIP-712 message fields
  bytes account = 1;              // 20-byte account address
  bytes spender = 2;              // 20-byte spender address
  bytes token = 3;                // 20-byte token address
  bytes allowance = 4;            // uint160 (big-endian bytes, minimal encoding)
  uint64 period = 5;              // uint48 in message, fits in uint64
  uint64 start = 6;               // uint48
  uint64 end = 7;                 // uint48
  bytes salt = 8;                 // 32-byte salt
  bytes extra_data = 9;           // extraData (may be empty for "0x")

  // EIP-712 domain fields
  bytes verifying_contract = 10;  // 20-byte verifyingContract
  string domain_name = 11;        // Domain name (e.g., "Spend Permission Manager")
  string domain_version = 12;     // Domain version (e.g., "1")
}

message ReceiveWithAuthorization {
  // EIP-712 message fields
  bytes from = 1;                 // 20-byte from address
  bytes to = 2;                   // 20-byte to address
  bytes value = 3;                // uint256 (big-endian bytes, minimal encoding)
  bytes valid_after = 4;          // uint256 (typically 0)
  bytes valid_before = 5;         // uint256 (timestamp)
  bytes nonce = 6;                // bytes32

  // EIP-712 domain fields
  bytes verifying_contract = 7;   // 20-byte USDC contract
  string domain_name = 8;         // Domain name (e.g., "USDC")
  string domain_version = 9;      // Domain version (e.g., "2")
}

message GenericTypedData {
  bytes typed_data_json = 1;      // UTF-8 JSON-encoded EIP-712 TypedData
}

Type Detection (Normative)

Encoders MUST detect signature types for EIP-712 typed data in this order:

  1. SpendPermission: primaryType === "SpendPermission" and domain has verifyingContract
  2. ReceiveWithAuthorization: primaryType === "ReceiveWithAuthorization" and domain has verifyingContract
  3. Generic: Any other wallet_sign request with typed data

Encoding Algorithm

Type field normalization: The type field in EIP-7871 requests indicates EIP-712 typed data. Encoders MUST accept the following variants as equivalent to EIP-712:

  • String "0x01" (canonical)
  • String "0x1" (no leading zero)
  • Number 1
  • Missing type field (assume EIP-712 if data contains typed data structure)

Chain ID validation: EIP-712 typed data contains chainId in both the params and the domain. Encoders MUST:

  • Verify that params.chainId (hex string) and domain.chainId (number or hex string) represent the same chain
  • Reject requests where these values conflict
  • Use the params-level chainId for the RpcLinkPayload.chain_id field
  1. Parse EIP-7871 wallet_sign request
  2. Validate it contains EIP-712 typed data (check type field or inspect data structure)
  3. Validate chain ID consistency (params vs domain)
  4. Detect signature type (see Type Detection section above)
  5. Extract fields:
    • SpendPermission: Extract all message fields + domain fields
    • ReceiveWithAuthorization: Extract all message fields + domain fields
    • Generic: Serialize entire typed data as JSON
  6. Set version if present
  7. Populate RpcLinkPayload with shortcut_id = 2, chain_id, and capabilities

Decoding Algorithm

  1. Validate shortcut_id = 2

  2. Switch on type and reconstruct EIP-712 typed data:

    SpendPermission:

    {
      "types": {
        "EIP712Domain": [
          {"name": "name", "type": "string"},
          {"name": "version", "type": "string"},
          {"name": "chainId", "type": "uint256"},
          {"name": "verifyingContract", "type": "address"}
        ],
        "SpendPermission": [
          {"name": "account", "type": "address"},
          {"name": "spender", "type": "address"},
          {"name": "token", "type": "address"},
          {"name": "allowance", "type": "uint160"},
          {"name": "period", "type": "uint48"},
          {"name": "start", "type": "uint48"},
          {"name": "end", "type": "uint48"},
          {"name": "salt", "type": "uint256"},
          {"name": "extraData", "type": "bytes"}
        ]
      },
      "domain": {
        "name": domain_name,
        "version": domain_version,
        "chainId": chain_id (as number),
        "verifyingContract": "0x" + verifying_contract.hex()
      },
      "primaryType": "SpendPermission",
      "message": {
        "account": "0x" + account.hex(),
        "spender": "0x" + spender.hex(),
        "token": "0x" + token.hex(),
        "allowance": "0x" + allowance.hex(),
        "period": period (as number),
        "start": start (as number),
        "end": end (as number),
        "salt": "0x" + salt.hex(),
        "extraData": extra_data.length > 0 ? "0x" + extra_data.hex() : "0x"
      }
    }

    ReceiveWithAuthorization:

    {
      "types": {
        "EIP712Domain": [
          {"name": "name", "type": "string"},
          {"name": "version", "type": "string"},
          {"name": "chainId", "type": "uint256"},
          {"name": "verifyingContract", "type": "address"}
        ],
        "ReceiveWithAuthorization": [
          {"name": "from", "type": "address"},
          {"name": "to", "type": "address"},
          {"name": "value", "type": "uint256"},
          {"name": "validAfter", "type": "uint256"},
          {"name": "validBefore", "type": "uint256"},
          {"name": "nonce", "type": "bytes32"}
        ]
      },
      "domain": {
        "name": domain_name,
        "version": domain_version,
        "chainId": chain_id (as number),
        "verifyingContract": "0x" + verifying_contract.hex()
      },
      "primaryType": "ReceiveWithAuthorization",
      "message": {
        "from": "0x" + from.hex(),
        "to": "0x" + to.hex(),
        "value": "0x" + value.hex(),
        "validAfter": "0x" + valid_after.hex(),
        "validBefore": "0x" + valid_before.hex(),
        "nonce": "0x" + nonce.hex()
      }
    }

    Generic: Parse typed_data_json directly as the EIP-712 TypedData structure

  3. Reconstruct EIP-7871 request:

    {
      "method": "wallet_sign",
      "params": [{
        "version": version || "1",
        "chainId": "0x" + chainId.toString(16),
        "type": "0x01",
        "data": { /* EIP-712 TypedData from step 2 */ },
        "capabilities": parseCapabilities(capabilities)
      }]
    }

Example: SpendPermission

Input (EIP-7871):

{
  "method": "wallet_sign",
  "params": [{
    "version": "1",
    "chainId": "0x14a34",
    "type": "0x01",
    "data": {
      "types": {
        "SpendPermission": [
          {"name": "account", "type": "address"},
          {"name": "spender", "type": "address"},
          {"name": "token", "type": "address"},
          {"name": "allowance", "type": "uint160"},
          {"name": "period", "type": "uint48"},
          {"name": "start", "type": "uint48"},
          {"name": "end", "type": "uint48"},
          {"name": "salt", "type": "uint256"},
          {"name": "extraData", "type": "bytes"}
        ]
      },
      "domain": {
        "name": "Spend Permission Manager",
        "version": "1",
        "chainId": 84532,
        "verifyingContract": "0xf85210b21cc50302f477ba56686d2019dc9b67ad"
      },
      "primaryType": "SpendPermission",
      "message": {
        "account": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "spender": "0x8d9F34934dc9619e5DC3Df27D0A40b4A744E7eAa",
        "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
        "allowance": "0x2710",
        "period": 281474976710655,
        "start": 0,
        "end": 1914749767655,
        "salt": "0x2d6688aae9435fb91ab0a1fe7ea54ec3ffd86e8e18a0c17e1923c467dea4b75f",
        "extraData": "0x"
      }
    }
  }]
}

Encoded (pseudocode):

{
  protocolVersion: 1,
  chainId: 84532,
  shortcutId: 2,
  shortcutVersion: 0,
  body: {
    case: 'walletSign',
    value: {
      type: SignType.SPEND_PERMISSION,
      signatureData: {
        case: 'spendPermission',
        value: {
          account: hex('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'),
          spender: hex('8d9F34934dc9619e5DC3Df27D0A40b4A744E7eAa'),
          token: hex('036CbD53842c5426634e7929541eC2318f3dCF7e'),
          allowance: bigIntToBytes(10000n),
          period: 281474976710655n,
          start: 0n,
          end: 1914749767655n,
          salt: hex('2d6688aae9435fb91ab0a1fe7ea54ec3ffd86e8e18a0c17e1923c467dea4b75f'),
          extraData: new Uint8Array(0), // empty bytes for "0x"
          verifyingContract: hex('f85210b21cc50302f477ba56686d2019dc9b67ad'),
          domainName: 'Spend Permission Manager',
          domainVersion: '1'
        }
      },
      version: '1'
    }
  }
}

Canonical Encodings (Normative)

All shortcuts MUST follow these encoding rules:

Addresses

  • Format: exactly 20 bytes, big-endian raw bytes (no 0x prefix on wire)
  • Encoders MUST normalize input hex to lowercase before conversion
  • Decoders MUST reject lengths other than 20 bytes

Integer Amounts

  • Format: big-endian minimal bytes with NO leading zero octets
  • Zero encoding: Encoders SHOULD encode zero as a single 0x00 byte, but MAY omit the field (proto3 optimization)
  • Zero decoding: Decoders MUST treat missing fields, empty bytes (length 0), and 0x00 (1 byte) as zero
  • Leading zeros: Decoders MUST reject encodings with unnecessary leading zeros (e.g., 0x0001 for value 1)
  • Examples:
    • 0 β†’ 0x00 (1 byte) or omitted field
    • 255 β†’ 0xff (1 byte)
    • 256 β†’ 0x0100 (2 bytes)
    • 5000000 β†’ 0x4c4b40 (3 bytes)

Fixed-Size Fields

  • salt, nonce: MUST be exactly 32 bytes
  • Decoders MUST reject other lengths

Calldata

  • Format: raw bytes (may be empty)
  • Empty calldata: zero-length bytes field

Chain ID

  • Format: uint32 (varint-encoded on wire)
  • MUST be the canonical numeric chain ID (e.g., 1 for Ethereum, 8453 for Base)
  • Sufficient for all realistic EVM chain IDs (max 4,294,967,295)

Strings

  • Format: UTF-8 without BOM
  • Used for: method, version, domain_name, domain_version, JSON in capabilities

Capabilities

  • Storage format: Each capability value is stored as the UTF-8 bytes of a JSON-serialized value (RFC 8259)
  • Allowed types: Capability values MAY be any valid JSON value: objects {}, arrays [], strings, numbers, booleans, or null
  • Object capabilities: For object values, encoders SHOULD sort keys alphabetically for deterministic encoding
  • Decoding: Decoders MUST parse the UTF-8 bytes as JSON and validate the result is well-formed
  • Size limits: Implementations SHOULD enforce size limits (RECOMMENDED ≀ 4 KB per entry, ≀ 16 KB total)
  • Malformed data: Invalid UTF-8 or JSON syntax errors MUST cause rejection

Example capability encodings:

// Example 1: Object value
{ "dataCallback": { "callbackURL": "https://example.com", "events": ["initiated"] } }
// Stored as:
capabilities["dataCallback"] = utf8Encode('{"callbackURL":"https://example.com","events":["initiated"]}')

// Example 2: String value
{ "order_id": "ORDER-123" }
// Stored as:
capabilities["order_id"] = utf8Encode('"ORDER-123"') // JSON string includes quotes

// Example 3: Number value
{ "tip_bps": 50 }
// Stored as:
capabilities["tip_bps"] = utf8Encode('50') // No quotes for numbers

Complete Encoding Algorithm

Encoders MUST follow this algorithm:

  1. Validate input: Ensure request is well-formed wallet_sendCalls, wallet_sign, or generic JSON-RPC
  2. Select shortcut:
    • wallet_sendCalls β†’ shortcut 1 (detect type: ERC-20/Native/Generic)
    • wallet_sign with EIP-712 β†’ shortcut 2 (detect type: SpendPermission/ReceiveWithAuthorization/Generic)
    • Any other method β†’ shortcut 0 (generic)
  3. Extract fields: Populate the appropriate protobuf message per shortcut rules
  4. Serialize: Encode using Protocol Buffers v3
  5. Compress:
    • Try Brotli compression
    • If brotliSize + 1 < uncompressedSize + 1: prepend 0x01 to compressed data
    • Otherwise: prepend 0x00 to uncompressed data
  6. Encode: Base64url encode the result WITHOUT padding

Complete Decoding Algorithm

Decoders MUST follow this algorithm:

  1. Base64url decode: Reject invalid characters or malformed padding
  2. Check compression flag (first byte):
    • 0x01 β†’ Brotli decompress remaining bytes
    • 0x00 β†’ use remaining bytes directly
    • Other β†’ reject with error
  3. Protobuf decode: Deserialize into RpcLinkPayload
  4. Validate protocol version: Reject if protocol_version != 1
  5. Dispatch by shortcut:
    • shortcut_id = 0 β†’ decode GenericJsonRpc
    • shortcut_id = 1 β†’ decode WalletSendCalls (dispatch by type)
    • shortcut_id = 2 β†’ decode WalletSign (dispatch by type)
    • Other β†’ reject with "unsupported shortcut" error
  6. Reconstruct RPC request: Build JSON-RPC request per shortcut rules
  7. Validate: Ensure reconstructed request is well-formed

Error Handling (Normative)

Decoders MUST return explicit errors and MUST NOT proceed with partially valid data for:

  • Unsupported protocol version: Unknown protocol_version
  • Invalid Base64url: Characters outside alphabet or invalid padding
  • Unknown compression flag: Flag byte not 0x00 or 0x01
  • Decompression failure: Brotli error, timeout, or corrupted data
  • Invalid protobuf structure: Missing required oneofs, wrong field lengths
  • Unsupported shortcut: Unknown or unimplemented shortcut_id
  • Non-canonical integers: Leading zero octets in amount/value fields
  • Wrong address length: Address fields not exactly 20 bytes
  • Wrong salt/nonce length: Fixed-size fields not exactly 32 bytes
  • Malformed JSON: Invalid UTF-8 or JSON syntax in params_json, typed_data_json, or capabilities
  • Capability size exceeded: Capability values exceeding implementation limits

Capability Extensions

This standard includes an extension point for metadata via the capabilities map. Each capability value MUST be a valid JSON value (object, array, string, number, boolean, or null) serialized as UTF-8 bytes. See Capabilities section for encoding rules.

Data Callback Capability (ERC-8026)

The dataCallback capability enables wallet→server event notifications for enhanced UX and allows apps to request user data. It is defined in ERC-8026.

Structure:

  • callbackURL: HTTPS URL for webhook events
  • events: Array of event objects, each with:
    • type: One of "initiated", "preSign", or "postSign"
    • context: Optional app-defined data (any JSON value)
    • requests: Optional array for preSign events (specifies what user data to collect)

Encoding example with data requests:

{
  "capabilities": {
    "dataCallback": {
      "callbackURL": "https://example.com/callback",
      "events": [
        {
          "type": "initiated",
          "context": { "orderId": "ORDER-123" }
        },
        {
          "type": "preSign",
          "requests": [
            { "type": "email" },
            { "type": "physicalAddress", "optional": true }
          ],
          "context": { "shippingTier": "express" }
        },
        {
          "type": "postSign"
        }
      ]
    }
  }
}

Simpler example (notifications only):

{
  "capabilities": {
    "dataCallback": {
      "callbackURL": "https://example.com/callback",
      "events": [
        { "type": "initiated" },
        { "type": "postSign" }
      ]
    }
  }
}

Security requirements

  • Wallets MUST use HTTPS for callback URLs
  • Wallets SHOULD implement timeouts and retry logic with exponential backoff

Note: Full webhook payload schemas, data types, validation rules, and behavior are defined in ERC-8026. This ERC only specifies how to encode the capability in the payload format. The capability value is stored as UTF-8 JSON bytes per the Capabilities encoding rules.

Size Savings Analysis (Informative)

Typical compression results (payload size in bytes, including compression flag and Base64url overhead):

Transaction TypeJSON (minified)Protobuf only (0x00)Protobuf + Brotli (0x01)
ERC-20 Transfer~280-350~120-150~90-120
Native Transfer~180-220~80-100~60-80
SpendPermission~650-800~200-250~150-200

Compression ratio: 60-80% size reduction for optimized shortcuts compared to raw JSON.

Rationale

Protocol Buffers

Protocol Buffers was chosen for serialization because:

  • Compact binary format: 50-70% smaller than JSON
  • Schema evolution: Fields can be added without breaking compatibility
  • Wide adoption: Mature libraries in all major languages
  • Type safety: Prevents encoding errors
  • Efficient encoding: Varint compression for numeric fields

Brotli Compression

Brotli was selected as the optional compression layer because:

  • Superior compression: 15-25% better than gzip for structured data
  • Browser support: Native support in modern browsers (93%+ global)
  • Optimized for text: Works well with protobuf's wire format

Optional compression supports:

  • Environments without Brotli (use 0x00)
  • Very small payloads where compression overhead exceeds benefits
  • Applications prioritizing speed over size

Base64url

Base64url (RFC 4648) was chosen for final encoding because:

  • URL-safe: Works in query parameters, fragments, paths
  • No padding: Removes unnecessary trailing = characters
  • Universal support: Available in all programming environments
  • QR-friendly: Efficient encoding for QR codes (alphanumeric mode)

Shortcut Architecture

Separating shortcuts from the core format provides:

  • Stability: Core format remains unchanged as new shortcuts are added
  • Flexibility: Applications can implement only the shortcuts they need
  • Optimization: Method-specific encodings achieve maximum compression
  • Backward compatibility: New shortcuts don't break existing implementations

No Hardcoded Enums

This ERC deliberately avoids hardcoded chain/token enums (unlike earlier experiments) because:

  • Maintainability: No need to update the standard as new chains/tokens emerge
  • Simplicity: Fewer lookup tables and edge cases
  • Universal compatibility: Works with any EVM chain and any token
  • Size trade-off: Saves 15-20 bytes per enum, but adds specification complexity

Raw addresses and chain IDs are simple, universal, and sufficient for this use case.

Minimal Core Schema

Transaction metadata (payee info, order details, tips) is intentionally excluded from the core schema:

  • Broad applicability: Different applications have different metadata needs
  • Maintainability: Reduces core specification complexity
  • Flexibility: Capabilities map allows application-specific extensions
  • Interoperability: Focused core makes implementation easier

Applications requiring metadata can use the capabilities map without modifying the protocol.

Backwards Compatibility

  • Any wallet_sendCalls (EIP-5792) or wallet_sign (EIP-7871) request can be encoded via shortcuts 1/2 or the generic shortcut (0)
  • Decoded requests are valid inputs to the original RPC methods
  • Wallets without compression support can receive uncompressed payloads (0x00)
  • This ERC introduces a new format; it does not modify existing RPC standards

Security Considerations

Request Validation

Wallets MUST treat decoded requests as untrusted input and apply standard safety checks:

  • Transaction simulation: Simulate before presenting to user
  • Address validation: Verify addresses against expected checksums or ENS
  • Balance checks: Ensure sufficient balance for transactions
  • Gas estimation: Warn on unusually high gas costs
  • Contract analysis: Flag interactions with unverified contracts
  • Phishing detection: Check against known malicious addresses

Privacy

  • Payloads are plaintext once decoded; do not embed sensitive data (e.g., private keys, passwords)
  • Use HTTPS for any URLs in capabilities
  • Consider link expiration for time-sensitive requests

Front-Running and MEV

This format is suitable for:

  • Simple transfers and payments
  • Signatures with fixed parameters
  • Idempotent operations

This format is NOT suitable for:

  • Competitive flows (DEX trades, auctions, liquidations)
  • Time-sensitive operations with variable outcomes
  • Any transaction where public visibility before execution creates exploitable MEV

Resource Limits

Implementations MUST enforce limits to prevent resource exhaustion:

  • Decompression timeout: RECOMMENDED ≀ 5 seconds
  • Parsing timeout: RECOMMENDED ≀ 2 seconds
  • Capability size: RECOMMENDED ≀ 4 KB per entry, ≀ 16 KB total
  • Call count: RECOMMENDED ≀ 100 calls for generic calls
  • Recursion depth: Bound JSON parsing depth (RECOMMENDED ≀ 32 levels)

Callback Security

When implementing ERC-8026 callbacks:

  • Use HTTPS exclusively
  • Implement timeouts and retry limits
  • Validate callback response signatures
  • Never trust callback data without verification
  • Log all callback interactions for audit

Test Vectors

Note: The following test vectors are provided as implementation guidance. The Base64url-encoded outputs represent the complete payload after protobuf serialization, Brotli compression, and Base64url encoding. Implementers SHOULD verify that their encoders produce structurally equivalent outputs (same protobuf structure after decoding) and that all fields roundtrip correctly.

Test Vector 1: ERC-20 Transfer (Shortcut 1)

Input (EIP-5792):

{
  "method": "wallet_sendCalls",
  "params": [{
    "version": "1.0",
    "chainId": "0x2105",
    "calls": [{
      "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "data": "0xa9059cbb000000000000000000000000fe21034794a5a574b94fe4fdfd16e005f1c96e5100000000000000000000000000000000000000000000000000000000004c4b40",
      "value": "0x0"
    }]
  }]
}

Protobuf representation (before serialization):

protocol_version: 1
chain_id: 8453
shortcut_id: 1
shortcut_version: 0
wallet_send_calls {
  type: ERC20_TRANSFER
  erc20_transfer {
    token: [20 bytes: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913]
    recipient: [20 bytes: 0xfe21034794a5a574b94fe4fdfd16e005f1c96e51]
    amount: [3 bytes: 0x4c4b40]
  }
  version: "1.0"
}

Expected output (Base64url, Brotli-compressed):

AQj1QRABGgQxLjBSORIUgzWJ_NbttpgvHHMtT3cbVL2gKRMaFP4hA0eUpapXS5T-_RbgBfHJblUiA0xLQA

(Note: Actual output may vary slightly depending on Brotli implementation; decoders must produce the same protobuf structure)

Test Vector 2: Native Transfer (Shortcut 1)

Input (EIP-5792):

{
  "method": "wallet_sendCalls",
  "params": [{
    "version": "1.0",
    "chainId": "0x1",
    "calls": [{
      "to": "0xfe21034794a5a574b94fe4fdfd16e005f1c96e51",
      "data": "0x",
      "value": "0xde0b6b3a7640000"
    }]
  }]
}

Protobuf representation:

protocol_version: 1
chain_id: 1
shortcut_id: 1
shortcut_version: 0
wallet_send_calls {
  type: NATIVE_TRANSFER
  native_transfer {
    recipient: [20 bytes: 0xfe21034794a5a574b94fe4fdfd16e005f1c96e51]
    amount: [8 bytes: 0x0de0b6b3a7640000]
  }
  version: "1.0"
}

Expected output (Base64url, Brotli-compressed):

AQgBEAEaBC4wWh0SFP4hA0eUpapXS5T-_RbgBfHJblUaCQjgtrOn4AAAA

Test Vector 3: SpendPermission (Shortcut 2)

Input (EIP-7871):

{
  "method": "wallet_sign",
  "params": [{
    "version": "1",
    "chainId": "0x14a34",
    "type": "0x01",
    "data": {
      "types": {
        "SpendPermission": [
          {"name": "account", "type": "address"},
          {"name": "spender", "type": "address"},
          {"name": "token", "type": "address"},
          {"name": "allowance", "type": "uint160"},
          {"name": "period", "type": "uint48"},
          {"name": "start", "type": "uint48"},
          {"name": "end", "type": "uint48"},
          {"name": "salt", "type": "uint256"},
          {"name": "extraData", "type": "bytes"}
        ]
      },
      "domain": {
        "name": "Spend Permission Manager",
        "version": "1",
        "chainId": 84532,
        "verifyingContract": "0xf85210b21cc50302f477ba56686d2019dc9b67ad"
      },
      "primaryType": "SpendPermission",
      "message": {
        "account": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        "spender": "0x8d9F34934dc9619e5DC3Df27D0A40b4A744E7eAa",
        "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
        "allowance": "0x2710",
        "period": 281474976710655,
        "start": 0,
        "end": 1914749767655,
        "salt": "0x2d6688aae9435fb91ab0a1fe7ea54ec3ffd86e8e18a0c17e1923c467dea4b75f",
        "extraData": "0x"
      }
    }
  }]
}

Protobuf representation:

protocol_version: 1
chain_id: 84532
shortcut_id: 2
shortcut_version: 0
wallet_sign {
  type: SPEND_PERMISSION
  spend_permission {
    account: [20 bytes: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]
    spender: [20 bytes: 0x8d9F34934dc9619e5DC3Df27D0A40b4A744E7eAa]
    token: [20 bytes: 0x036CbD53842c5426634e7929541eC2318f3dCF7e]
    allowance: [2 bytes: 0x2710]
    period: 281474976710655
    start: 0
    end: 1914749767655
    salt: [32 bytes: 0x2d6688aae9435fb91ab0a1fe7ea54ec3ffd86e8e18a0c17e1923c467dea4b75f]
    extra_data: [0 bytes: empty]
    verifying_contract: [20 bytes: 0xf85210b21cc50302f477ba56686d2019dc9b67ad]
    domain_name: "Spend Permission Manager"
    domain_version: "1"
  }
  version: "1"
}

Expected output (Base64url, Brotli-compressed):

AQi0lAUQAhoBMVJWCAESFKqqqqqqqqqqqqqqqqqqqqqqqqqqGhSJ-TSTI8mRnlzhPfJ9CkC0p0R-qiIDNs6cAzZL1TOFY0Zmbu5JljHpM-gv0sXvjGXa0_I-EMiALWaIqulDX7katpH-6U7zPfhui6HAXHkyNHrpK3X_SRRTcGVuZCBQZXJtaXNzaW9uIE1hbmFnZXJaATFiBPiJELISzFDML0d3ukVobaAdybn2rQ

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth