c8ntinuum accounts use Ethereum-compatible eth_secp256k1 keys. The same account can be represented as an EVM hex address (0x...) or a Cosmos Bech32 address (c8...). Exchange and custody systems can generate deposit addresses offline using standard Ethereum key derivation. The JavaScript and Java examples below are excerpted from compile-tested samples. See Tested Exchange Examples.

Address rules

ItemValue
Account key typeeth_secp256k1
EVM address20-byte Ethereum address, displayed as 0x...
Cosmos account addressBech32 with c8 prefix
Mainnet EVM chain ID2184 / 0x888
Testnet EVM chain ID2185 / 0x889
Recommended wallet pathEthereum-compatible path, usually m/44'/60'/0'/0/index
Use 0x... for EVM JSON-RPC, contracts, wallet prompts, and token transfers. Use c8... for Cosmos REST/LCD account queries or IBC-facing UX. Both formats point to the same 20 account bytes.

Memos and destination tags

Memo support depends on the transfer path, not the address format.
Transfer pathTransaction memo supportIntegration guidance
EVM native CTM transferNo standard memo or destination-tag field. Standard account-to-account transfers should use empty data.Use unique deposit addresses and credit by transaction hash plus recipient address.
EVM ERC-20 transferNo memo or destination-tag field in the standard transfer(address,uint256) call.Use unique deposit addresses. If routing metadata is required, use an application-specific contract flow instead of a standard ERC-20 transfer.
Cosmos SDK transferYes. Cosmos transactions can include a transaction-level memo.Support this only for Cosmos-native send paths, and make sure the scanner reads, stores, and validates the transaction memo.
IBC ICS-20 transferYes. ICS-20 transfers include a message-level memo parameter.Treat the memo as transfer message data for IBC routing or application metadata, not as part of the recipient address.
For exchange deposits, the safest default is one deposit address per user or account. Shared omnibus addresses require Cosmos-native or IBC memo processing and are not supported by standard EVM native or ERC-20 transfer semantics.

Activation behavior

There is no separate c8ntinuum-specific address activation transaction documented in this source set. A generated address is valid before it receives funds. Operationally, an address becomes visible to most account queries after it has a balance, nonce, code, or other on-chain account state. For deposit systems:
  • You can assign unused generated addresses before funding.
  • Treat an address as activated after the first successful funding transaction or account-state query returns data.
  • Validate syntax before accepting a withdrawal destination, but do not require the destination to already exist on-chain.
  • Require native CTM on sender accounts before constructing outgoing transactions because gas is paid in native CTM.

JavaScript

Install:
npm install ethers @scure/base
Generate keys, derive addresses, and validate both formats:
import { Wallet, HDNodeWallet, getAddress, isAddress, getBytes, hexlify } from "ethers";
import { bech32 } from "@scure/base";

function hexToC8(hexAddress) {
  const checksummed = getAddress(hexAddress);
  return bech32.encode("c8", bech32.toWords(getBytes(checksummed)));
}

function c8ToHex(c8Address) {
  const decoded = bech32.decode(c8Address);
  if (decoded.prefix !== "c8") throw new Error("invalid c8ntinuum prefix");
  const bytes = new Uint8Array(bech32.fromWords(decoded.words));
  if (bytes.length !== 20) throw new Error("invalid account byte length");
  return getAddress(hexlify(bytes));
}

function validateC8ntinuumAddress(address) {
  if (address.startsWith("0x")) return isAddress(address);
  if (address.startsWith("c8")) {
    try {
      c8ToHex(address);
      return true;
    } catch {
      return false;
    }
  }
  return false;
}

const randomWallet = Wallet.createRandom();
console.log({
  privateKey: randomWallet.privateKey,
  publicKey: randomWallet.signingKey.publicKey,
  evmAddress: randomWallet.address,
  bech32Address: hexToC8(randomWallet.address),
});

const mnemonic = Wallet.createRandom().mnemonic.phrase;
const hdWallet = HDNodeWallet.fromPhrase(mnemonic, undefined, "m/44'/60'/0'/0/0");
console.log(validateC8ntinuumAddress(hdWallet.address));

Java

Dependencies: web3j for Ethereum keys. The tested Java sample includes a small Bech32 helper for c8... conversion.
import java.math.BigInteger;
import org.web3j.crypto.ECKeyPair;
import org.web3j.crypto.Keys;
import org.web3j.crypto.WalletUtils;
import org.web3j.utils.Numeric;

public final class C8ntinuumAddressDemo {
  static String hexToC8(String hexAddress) {
    String checksum = Keys.toChecksumAddress(hexAddress);
    byte[] addressBytes = Numeric.hexStringToByteArray(checksum);
    return Bech32.encode("c8", addressBytes);
  }

  static String c8ToHex(String c8Address) {
    byte[] addressBytes = Bech32.decodeToBytes(c8Address, "c8");
    if (addressBytes.length != 20) {
      throw new IllegalArgumentException("invalid account byte length");
    }
    return Keys.toChecksumAddress(Numeric.toHexString(addressBytes));
  }

  static boolean isValidC8ntinuumAddress(String address) {
    try {
      if (address.startsWith("0x")) return WalletUtils.isValidAddress(address);
      if (address.startsWith("c8")) {
        c8ToHex(address);
        return true;
      }
      return false;
    } catch (Exception error) {
      return false;
    }
  }

  public static void main(String[] args) throws Exception {
    ECKeyPair keyPair = Keys.createEcKeyPair();
    BigInteger privateKey = keyPair.getPrivateKey();
    BigInteger publicKey = keyPair.getPublicKey();

    String evmAddress = Keys.toChecksumAddress("0x" + Keys.getAddress(publicKey));
    String c8Address = hexToC8(evmAddress);

    System.out.println("privateKey=0x" + privateKey.toString(16));
    System.out.println("publicKey=0x" + publicKey.toString(16));
    System.out.println("evmAddress=" + evmAddress);
    System.out.println("c8Address=" + c8Address);
    System.out.println("valid=" + isValidC8ntinuumAddress(c8Address));
  }
}

Go

Dependencies: go-ethereum for keys and cosmos-sdk for Bech32 conversion.
package main

import (
	"crypto/ecdsa"
	"fmt"

	"github.com/cosmos/cosmos-sdk/types/bech32"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
)

func HexToC8(hexAddress string) (string, error) {
	if !common.IsHexAddress(hexAddress) {
		return "", fmt.Errorf("invalid EVM address")
	}
	addr := common.HexToAddress(hexAddress)
	return bech32.ConvertAndEncode("c8", addr.Bytes())
}

func C8ToHex(c8Address string) (common.Address, error) {
	prefix, bytes, err := bech32.DecodeAndConvert(c8Address)
	if err != nil {
		return common.Address{}, err
	}
	if prefix != "c8" || len(bytes) != common.AddressLength {
		return common.Address{}, fmt.Errorf("invalid c8ntinuum account address")
	}
	return common.BytesToAddress(bytes), nil
}

func main() {
	privateKey, err := crypto.GenerateKey()
	if err != nil {
		panic(err)
	}

	publicKey := privateKey.Public().(*ecdsa.PublicKey)
	evmAddress := crypto.PubkeyToAddress(*publicKey)
	c8Address, err := HexToC8(evmAddress.Hex())
	if err != nil {
		panic(err)
	}

	fmt.Printf("privateKey=0x%x\n", crypto.FromECDSA(privateKey))
	fmt.Printf("publicKey=0x%x\n", crypto.FromECDSAPub(publicKey))
	fmt.Println("evmAddress=", evmAddress.Hex())
	fmt.Println("c8Address=", c8Address)
	fmt.Println("isEvmValid=", common.IsHexAddress(evmAddress.Hex()))
}

Rust

Dependencies: alloy for keys and address parsing, plus bech32 for c8... conversion.
use alloy::primitives::{Address, hex};
use alloy::signers::local::PrivateKeySigner;
use bech32::{self, FromBase32, ToBase32, Variant};

fn hex_to_c8(address: Address) -> eyre::Result<String> {
    Ok(bech32::encode("c8", address.as_slice().to_base32(), Variant::Bech32)?)
}

fn c8_to_hex(c8_address: &str) -> eyre::Result<Address> {
    let (prefix, words, variant) = bech32::decode(c8_address)?;
    if prefix != "c8" || variant != Variant::Bech32 {
        eyre::bail!("invalid c8ntinuum prefix or encoding");
    }
    let bytes = Vec::<u8>::from_base32(&words)?;
    if bytes.len() != 20 {
        eyre::bail!("invalid account byte length");
    }
    Ok(Address::from_slice(&bytes))
}

fn main() -> eyre::Result<()> {
    let signer = PrivateKeySigner::random();
    let address = signer.address();
    let c8_address = hex_to_c8(address)?;

    let strict = Address::parse_checksummed(&address.to_checksum(None), None)?;
    let round_trip = c8_to_hex(&c8_address)?;

    println!("privateKey=0x{}", hex::encode(signer.to_bytes()));
    println!("evmAddress={}", strict.to_checksum(None));
    println!("c8Address={}", c8_address);
    println!("roundTrip={}", round_trip.to_checksum(None));

    Ok(())
}

Validation checklist

  • Accept 0x... addresses only when they are 20 bytes and parse as valid Ethereum addresses.
  • Prefer EIP-55 checksum casing for stored and displayed 0x... addresses.
  • Accept c8... only when Bech32 decoding succeeds, the prefix is exactly c8, and the decoded byte length is 20.
  • Store a canonical account identifier internally, such as lowercase 20-byte hex, then derive display formats as needed.
  • Reject validator operator (c8valoper...) and consensus (c8valcons...) addresses for normal user deposits unless the product explicitly supports validator operations.