> ## Documentation Index
> Fetch the complete documentation index at: https://seilabs-docs-evm-cookbook.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Sei Network Accounts: Dual Address System Explained

> Understand how Sei's unified account system works with both EVM (0x) and Cosmos (sei1) addresses, including association methods, security considerations, and cross-environment interactions.

Every account on Sei has a unique public key. You can use this public key to
generate multiple wallet addresses, and these addresses are functionally the
same. They look different, but they both point to the same destination: your
account. Depending on the dApp, they may be used interchangeably. The difference
is like the difference between the numeral "2" and the word "two". They both
define the same value, but they may be used in different contexts.

* **"hex" address**: Starts with `0x` and is EVM-based.

* **"bech32" address**: Starts with `sei1` and is used for Cosmos functions.

<img src="https://mintcdn.com/seilabs-docs-evm-cookbook/IhEyu7GQ2MVOnsHK/assets/address-derivation.png?fit=max&auto=format&n=IhEyu7GQ2MVOnsHK&q=85&s=e12603840c95348b369ca91e1844299d" alt="Address derivation" width="1600" height="900" data-path="assets/address-derivation.png" />

Although these addresses look different, they share the same underlying
account. After the two addresses are linked through association (described
below), any action that you take with one address also affects the other.

After association, if you deposit funds into your EVM address, you can access
and use the same funds with your Sei address. The reverse is also true. The two
addresses behave as one account. Until the addresses are associated, they are
treated as separate accounts with separate balances (see **Before linking**
below).

Both addresses of an account are derived from the same **public key**. The chain can recognize the link between them only **after it knows the public key**. The chain learns the public key through association.

## Key points

### Before linking

* The Bech32 (`sei...`) and EVM (`0x...`) addresses are treated as **separate
  accounts**.
* They have separate balances until they are linked.
* If the EVM address receives Cosmos tokens before association, a temporary
  Cosmos Bech32 address holds them. The tokens transfer to the associated
  address when the addresses are linked.
* Some types of transactions are **not possible** (see the table below).

### After linking

* Both addresses show the same balance.
* Applications can query either address format.

## Wallet association and transfer limitations

Some actions are **not possible** before the wallets are associated:

* Transfers of **CW-based tokens** (for example, CW20, CW721, and CW1155) from a
  non-EVM wallet to an **unassociated EVM address**.
* Transfers of **ERC-based tokens** (for example, ERC-20, ERC-721, and ERC-1155)
  from an EVM wallet to an **unassociated Cosmos address**.

## Methods of association

| Method | Security risk | User action required |
| - | - | - |
| 1. Broadcast a transaction | Low | Association happens automatically |
| 2. Direct private key | High | Use the private key directly |
| 3. Signed message | Medium | Sign a predefined message to prove ownership *(recommended for wallets)* |
| 4. Public key | Low | Send a compressed public key for association |

<Info>Each method makes the **public key** known to the chain. The chain then associates the EVM-compatible and Bech32 addresses automatically. All four methods go through the on-chain `addr` precompile (`0x0000000000000000000000000000000000001004`) or the EVM ante handler. They are available on every Sei EVM RPC.</Info>

<Note>The legacy `sei_associate` JSON-RPC method is no longer available. Method 3 offers the same wallet-signed-message flow through the `addr` precompile.</Note>

You can also find constants for the `addr` precompile in the
[Sei-Chain/precompiles](https://github.com/sei-protocol/sei-chain/tree/main/precompiles/addr) repository.

## Method 1: Broadcast a transaction

* When an account broadcasts a transaction, such as a token transfer, its public
  key is recorded on-chain.
* After the public key is known, the EVM address and the Bech32 address are
  linked automatically.
* As a result, balances and transactions are accessible from both address
  formats.

## Method 2: Direct private key association

<Danger>**Security risk**: **High**. This method requires direct access to the private key. An exposed private key can compromise the wallet.</Danger>

This method uses the private key directly to interact with the network.

```ts theme={null}
import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { seiTestnet } from 'viem/chains';
import { ADDRESS_PRECOMPILE_ABI, ADDRESS_PRECOMPILE_ADDRESS } from '@sei-js/precompiles';

const PRIVATE_KEY = '<replace_with_private_key>';

const publicClient = createPublicClient({
	chain: seiTestnet,
	transport: http()
});
const client = createWalletClient({ chain: seiTestnet, transport: http() });

const account = privateKeyToAccount(PRIVATE_KEY);

const response = await client.writeContract({
	account,
	address: ADDRESS_PRECOMPILE_ADDRESS,
	abi: ADDRESS_PRECOMPILE_ABI,
	functionName: 'associate',
	args: ['0', '0', '0', 'example_message'],
	gasPrice: BigInt(100_000_000_000)
});
console.log(response);
```

## Method 3: Associate via signed message

<Warning>**Security risk**: **Medium**. This method requires you to sign a specific message with the private key.</Warning>

In this method, you sign a predefined message to prove that you own the account.

```ts theme={null}
import { createWalletClient, http, parseSignature, toHex } from 'viem';
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
import { seiTestnet } from 'viem/chains';
import { ADDRESS_PRECOMPILE_ABI, ADDRESS_PRECOMPILE_ADDRESS } from '@sei-js/precompiles';

const client = createWalletClient({ chain: seiTestnet, transport: http() });

const associate = async () => {
	const account = privateKeyToAccount('<replace_with_private_key>');
	const newPk = generatePrivateKey();
	const newAccount = privateKeyToAccount(newPk);

	const message = 'associate';
	const signature = await newAccount.signMessage({ message });
	const parsedSignature = parseSignature(signature);
	const customMessage = `\x19Ethereum Signed Message:\n${message.length}${message}`;

	const response = await client.writeContract({
		account,
		address: ADDRESS_PRECOMPILE_ADDRESS,
		abi: ADDRESS_PRECOMPILE_ABI,
		functionName: 'associate',
		args: [toHex(Number(parsedSignature.v) - 27), parsedSignature.r, parsedSignature.s, customMessage],
		gasPrice: BigInt(100_000_000_000)
	});
	console.log(response);
};

associate();
```

## Method 4: Associate via public key

<Info>**Security risk**: **Low**. This method uses the public key, which is less sensitive than the private key.</Info>

This method compresses the public key and sends it for association.

```ts theme={null}
import secp256k1 from 'secp256k1';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
import { seiTestnet } from 'viem/chains';
import { ADDRESS_PRECOMPILE_ABI, ADDRESS_PRECOMPILE_ADDRESS } from '@sei-js/precompiles';

const client = createWalletClient({ chain: seiTestnet, transport: http() });

const associateViaPubkey = async () => {
	const account = privateKeyToAccount('<replace_with_private_key>');
	const newPk = generatePrivateKey();
	const newAccount = privateKeyToAccount(newPk);

	const publicKeyBuffer = Buffer.from(newAccount.publicKey.slice(2), 'hex');
	const compressedPubKey = secp256k1.publicKeyConvert(publicKeyBuffer, true);

	const response = await client.writeContract({
		account,
		address: ADDRESS_PRECOMPILE_ADDRESS,
		abi: ADDRESS_PRECOMPILE_ABI,
		functionName: 'associatePubKey',
		args: [Buffer.from(compressedPubKey).toString('hex')],
		gasPrice: BigInt(100_000_000_000)
	});
	console.log(response);
};

associateViaPubkey();
```

## Query linked addresses

To resolve either side of an existing association, call the `addr` precompile at `0x0000000000000000000000000000000000001004` with a standard `eth_call`. The precompile is available on every Sei RPC.

### Fetch Bech32 address for an EVM address

```bash theme={null}
curl -X POST $SEIEVM -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "params": [{
    "to": "0x0000000000000000000000000000000000001004",
    "data": "0x0c3c20ed000000000000000000000000<evmAddressWithout0x>"
  }, "latest"],
  "id": 1
}'
```

The selector `0x0c3c20ed` is `getSeiAddr(address)`. The call returns the bech32 `sei1…` address as an ABI-encoded string. If the EVM address is not associated yet, the call reverts.

In TypeScript with viem:

```ts theme={null}
import { createPublicClient, http } from 'viem';
import { sei } from 'viem/chains';

const ADDR_PRECOMPILE = '0x0000000000000000000000000000000000001004';
const ADDR_ABI = [
  { name: 'getSeiAddr', type: 'function', stateMutability: 'view',
    inputs: [{ name: 'addr', type: 'address' }],
    outputs: [{ name: 'response', type: 'string' }] },
  { name: 'getEvmAddr', type: 'function', stateMutability: 'view',
    inputs: [{ name: 'addr', type: 'string' }],
    outputs: [{ name: 'response', type: 'address' }] },
] as const;

const client = createPublicClient({ chain: sei, transport: http() });

const seiAddr = await client.readContract({
  address: ADDR_PRECOMPILE,
  abi: ADDR_ABI,
  functionName: 'getSeiAddr',
  args: ['0x…'],
});
```

### Fetch EVM address for a Sei address

```ts theme={null}
const evmAddr = await client.readContract({
  address: ADDR_PRECOMPILE,
  abi: ADDR_ABI,
  functionName: 'getEvmAddr',
  args: ['sei1…'],
});
```

If the address has never been associated, the precompile reverts. Handle the revert as a "not linked" result instead of re-throwing it.

## Deriving addresses from the public key

Both address formats come from the same secp256k1 public key, but they use
**different hashing schemes**. The bech32 (`sei1…`) side follows the standard
Cosmos derivation (`SHA256` then `RIPEMD160` of the **compressed** public key).
The EVM (`0x…`) side follows the standard Ethereum derivation (`keccak256` of
the **uncompressed** public key without its `0x04` prefix byte).

### Sei address derivation

The Cosmos address is derived from the public key in these steps:

1. Take the **compressed** secp256k1 public key (33 bytes, with a first byte of `0x02` or `0x03`).
2. Hash it with `SHA256`.
3. Hash the result with `RIPEMD160` to get a 20-byte digest.
4. Encode that digest in Bech32 format with the `sei` prefix.

Example implementation:

```ts theme={null}
import { bech32 } from 'bech32';
import { sha256 } from '@noble/hashes/sha256';
import { ripemd160 } from '@noble/hashes/ripemd160';

/**
 * @param compressedPublicKey 33-byte secp256k1 pubkey in compressed form
 */
export function deriveSeiAddress(compressedPublicKey: Uint8Array): string {
  const digest = ripemd160(sha256(compressedPublicKey));
  return bech32.encode('sei', bech32.toWords(digest));
}
```

### EVM address derivation

The EVM-compatible address is derived in these steps:

1. Take the **uncompressed** secp256k1 public key (65 bytes, with a first byte of `0x04`).
2. Drop the leading `0x04` prefix byte, so that the input to the hash is the bare 64-byte `(x, y)` coordinate pair.
3. Hash these bytes with `keccak256`.
4. Take the **last 20 bytes** of the hash and format them as `0x…` hex.

Example implementation:

```ts theme={null}
import { keccak_256 } from '@noble/hashes/sha3';

/**
 * @param uncompressedPublicKey 65-byte secp256k1 pubkey in uncompressed form (leading 0x04)
 */
export function deriveEVMAddress(uncompressedPublicKey: Uint8Array): string {
  const hash = keccak_256(uncompressedPublicKey.slice(1));
  return `0x${Buffer.from(hash.slice(-20)).toString('hex')}`;
}
```

### Summary

* **Sei address**: `bech32('sei', RIPEMD160(SHA256(compressedPubKey)))` (20 bytes, Cosmos-standard derivation).
* **EVM address**: `'0x' + keccak256(uncompressedPubKey[1:])[-20:]` (the last 20 bytes of the keccak256 hash, Ethereum-standard derivation).
* The two formats share an account because the chain stores the **public key** itself on association. Either format can be derived deterministically from the public key.

### Why it works

Both formats are deterministic address schemes derived from the public key. The
public key is recorded on-chain through any of the four association methods
above, or implicitly through a first signed transaction. After that, the chain can derive
both formats itself and route any incoming reference to the same account.

### Recap

* Accounts are linked automatically when a transaction is broadcast. You can
  also associate them manually through the `addr` precompile (`associate` or
  `associatePubKey`).
* Both address formats share the same **public key**.
* After linking, dApps and tools can access balances consistently across both
  address formats.

## HD paths and coin types

When you derive a private key from a mnemonic phrase, the hierarchical
deterministic (HD) path has multiple parameters, including the coin type. The
coin type determines the blockchain ecosystem that the key is derived for. This
matters when you work with different wallets and blockchains.

### Coin type parameter

The second parameter in the HD path specifies the coin type, which the BIP-44
standard defines. This parameter identifies the blockchain ecosystem of the
derived keys.

* **Ethereum (coin type 60)**: Wallets such as MetaMask use coin type 60. The
  typical HD path for Ethereum is `m/44'/60'/0'/0/0`.
* **Cosmos (coin type 118)**: Wallets for Cosmos-based chains, such as OKX,
  use coin type 118. The typical HD path for Cosmos is `m/44'/118'/0'/0/0`.

### Implications

You cannot use an Ethereum mnemonic phrase (coin type 60) directly in a Cosmos
wallet (coin type 118) to access the same accounts. The HD path determines a
different set of keys for each coin type, so the derived addresses differ.

### Private key export

You can export your private key from MetaMask (derived with coin type 60) and
import it into any Cosmos wallet. This works because a derived private key can
be used across different blockchain ecosystems, if the receiving wallet supports
the import function. You can then manage your assets across various blockchains
with the same underlying cryptographic key.

### Example HD paths

* **Traditional Cosmos path**: `m/44'/118'/0'/0/0`
* **Traditional EVM path**: `m/44'/60'/0'/0/0`

## Generating wallets

### Deriving bech32 and hex addresses from pubkey

Sei derives a bech32 address (Cosmos and Tendermint style) and a hex address
(Ethereum style) from the same public key. Each format uses its own hashing
scheme. The bech32 address uses the Cosmos-standard
`SHA256` then `RIPEMD160`. The hex address uses the `keccak256` method, which is
common in EVM networks. These snippets have detailed comments. They show the
correct method to derive both bech32 and hex addresses from a given ECDSA
secp256k1 key:

<Accordion title="Python - from pubkey">
  ```python theme={null}
  import base64
  import json
  from hashlib import sha256, new as hashlib_new
  from coincurve import PublicKey
  from bech32 import bech32_encode, convertbits
  from Crypto.Hash import keccak

  # Example input, replace with the actual pubkey JSON string
  pubkey_json = '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"Agmik4xkmF57hNjzykYHP3gRu1Mpae4B5BCiwx7jmRzI"}'

  # Extract the base64-encoded key from the JSON-like string
  pubkey_dict = json.loads(pubkey_json)
  pubkey_base64 = pubkey_dict['key']

  # Decode the base64-encoded public key
  public_key_compressed = base64.b64decode(pubkey_base64)

  # Ensure the public key length is 33 bytes (compressed key format)
  if len(public_key_compressed) != 33:
      raise ValueError(f"Invalid public key length, expected 33 bytes for compressed format, got {len(public_key_compressed)}")

  # Debugging: Print the public key details
  print(f"Compressed public key (hex): {public_key_compressed.hex()}")

  # SHA-256 on the compressed public key
  sha256_digest = sha256(public_key_compressed).digest()
  # Debugging: Print SHA-256 digest
  print(f"SHA-256 Digest: {sha256_digest.hex()}")

  # RIPEMD-160 on SHA-256 hash
  ripemd160 = hashlib_new('ripemd160')
  ripemd160.update(sha256_digest)
  ripemd160_digest = ripemd160.digest()
  # Debugging: Print RIPEMD-160 digest
  print(f"RIPEMD-160 Digest: {ripemd160_digest.hex()}")

  # Convert the digest to 5-bit groups for Bech32 encoding
  five_bit_ripemd160 = convertbits(ripemd160_digest, 8, 5, pad=True)
  bech32_address = bech32_encode("sei", five_bit_ripemd160)

  print(f"Bech32 Cosmos Address: {bech32_address}")

  # Decompress the public key to 65 bytes
  public_key = PublicKey(public_key_compressed).format(compressed=False)

  # Debugging: Print the public key details
  print(f"Decompressed public key length: {len(public_key)}")
  print(f"Decompressed public key (hex): {public_key.hex()}")

  # Derive Ethereum Address using Keccak-256
  keccak_hash = keccak.new(digest_bits=256)
  keccak_hash.update(public_key[1:])  # Exclude the first byte (0x04)
  digest_keccak = keccak_hash.digest()
  eth_address = digest_keccak[-20:]
  eth_address_hex = '0x' + eth_address.hex()

  print(f"Ethereum Address: {eth_address_hex}")
  ```
</Accordion>

<Accordion title="TypeScript - from pubkey">
  ```typescript theme={null}
  import { fromBase64 } from '@cosmjs/encoding';
  import { sha256 } from '@noble/hashes/sha256';
  import { ripemd160 } from '@noble/hashes/ripemd160';
  import { keccak_256 } from '@noble/hashes/sha3';
  import { secp256k1 } from '@noble/curves/secp256k1';
  import { bech32 } from 'bech32';

  // Utility function to convert bits for Bech32 encoding
  function convertBits(data: Uint8Array, fromBits: number, toBits: number, pad: boolean): number[] {
    let acc = 0;
    let bits = 0;
    const result: number[] = [];
    const maxv = (1 << toBits) - 1;

    for (const value of data) {
      acc = (acc << fromBits) | value;
      bits += fromBits;
      while (bits >= toBits) {
        bits -= toBits;
        result.push((acc >> bits) & maxv);
      }
    }

    if (pad) {
      if (bits > 0) {
        result.push((acc << (toBits - bits)) & maxv);
      }
    } else if (bits >= fromBits || (acc << (toBits - bits)) & maxv) {
      throw new Error('Unable to convert bits');
    }

    return result;
  }

  // Define the prefix for the Bech32 address (e.g., "sei" for Sei network)
  const chainPrefix = 'sei';

  // Public key JSON string (replace with actual data)
  const pubkeyJson = '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"AiN+aFvHgjblWPaP9Er5p005JjPX3nj4I/+jA6W4BOho"}';

  // Parse the JSON string to extract the public key in Base64 format
  const pubkeyDict = JSON.parse(pubkeyJson);
  const pubkeyBase64 = pubkeyDict.key;

  console.log('Original public key JSON:', pubkeyJson);
  console.log('Parsed public key object:', pubkeyDict);
  console.log('Base64-encoded public key:', pubkeyBase64);

  // Decode the Base64-encoded public key to its compressed form
  const publicKeyCompressed = fromBase64(pubkeyBase64);

  console.log('Compressed public key (bytes):', publicKeyCompressed);
  console.log('Compressed public key (hex):', Buffer.from(publicKeyCompressed).toString('hex'));

  // Perform SHA-256 hashing on the compressed public key
  const sha256Digest = sha256(publicKeyCompressed);
  console.log('SHA-256 hash of public key (hex):', Buffer.from(sha256Digest).toString('hex'));

  // Perform RIPEMD-160 hashing on the SHA-256 digest
  const ripemd160Digest = ripemd160(sha256Digest);
  console.log('RIPEMD-160 hash of SHA-256 hash (hex):', Buffer.from(ripemd160Digest).toString('hex'));

  // Convert the RIPEMD-160 digest to a 5-bit array for Bech32 encoding
  const fiveBitArray = convertBits(ripemd160Digest, 8, 5, true);

  // Encode the 5-bit array into a Bech32 address with the specified prefix
  const bech32Address = bech32.encode(chainPrefix, fiveBitArray);

  console.log(`Bech32 Cosmos Address: ${bech32Address}`);

  // Decompress the public key to its uncompressed form (65 bytes) and exclude the first byte
  const publicKeyUncompressed = secp256k1.ProjectivePoint.fromHex(publicKeyCompressed).toRawBytes(false).slice(1);

  // Perform Keccak-256 hashing on the uncompressed public key to derive the Ethereum address
  const keccakHash = keccak_256(publicKeyUncompressed);
  const ethAddress = '0x' + Buffer.from(keccakHash.slice(-20)).toString('hex');

  console.log('Uncompressed public key (hex):', Buffer.from(publicKeyUncompressed).toString('hex'));
  console.log('Keccak-256 hash of uncompressed public key (hex):', Buffer.from(keccakHash).toString('hex'));
  console.log(`Ethereum Address: ${ethAddress}`);
  ```
</Accordion>

<Accordion title="TypeScript - full derivation from private key">
  ```typescript theme={null}
  import { sha256 } from '@noble/hashes/sha256';
  import { ripemd160 } from '@noble/hashes/ripemd160';
  import { keccak_256 } from '@noble/hashes/sha3';
  import { secp256k1 } from '@noble/curves/secp256k1';
  import { bech32 } from 'bech32';

  // Utility function to convert bits for Bech32 encoding
  function convertBits(data: Uint8Array, fromBits: number, toBits: number, pad: boolean): number[] {
    let acc = 0;
    let bits = 0;
    const result: number[] = [];
    const maxv = (1 << toBits) - 1;

    for (const value of data) {
      acc = (acc << fromBits) | value;
      bits += fromBits;
      while (bits >= toBits) {
        bits -= toBits;
        result.push((acc >> bits) & maxv);
      }
    }

    if (pad) {
      if (bits > 0) {
        result.push((acc << (toBits - bits)) & maxv);
      }
    } else if (bits >= fromBits || (acc << (toBits - bits)) & maxv) {
      throw new Error('Unable to convert bits');
    }

    return result;
  }

  // Function to generate addresses from a private key
  function generateAddresses(privateKeyHex: string): {
    seiAddress: string;
    ethAddress: string;
  } {
    // Ensure the private key is exactly 32 bytes long
    const privateKey = Uint8Array.from(Buffer.from(privateKeyHex.padStart(64, '0'), 'hex'));
    if (privateKey.length !== 32) {
      throw new Error('Private key must be 32 bytes long.');
    }

    // Derive the compressed public key from the private key
    const publicKey = secp256k1.getPublicKey(privateKey, true);
    const publicKeyBytes = publicKey;

    // Perform SHA-256 hashing on the compressed public key
    const sha256Digest = sha256(publicKeyBytes);

    // Perform RIPEMD-160 hashing on the SHA-256 digest
    const ripemd160Digest = ripemd160(sha256Digest);

    // Convert the RIPEMD-160 digest to a 5-bit array for Bech32 encoding
    const fiveBitArray = convertBits(ripemd160Digest, 8, 5, true);

    // Bech32 address with "sei" prefix
    const seiAddress = bech32.encode('sei', fiveBitArray, 256);

    // Derive the uncompressed public key from the private key and exclude the first byte
    const publicKeyUncompressed = secp256k1.getPublicKey(privateKey, false).slice(1);

    // Perform Keccak-256 hashing on the uncompressed public key to derive the Ethereum address
    const keccakHash = keccak_256(publicKeyUncompressed);
    const ethAddress = `0x${Buffer.from(keccakHash).slice(-20).toString('hex')}`;

    return { seiAddress, ethAddress };
  }

  // Example usage of the generateAddresses function
  const privateKeyHex = '907ab4bf7fc60cff';
  const { seiAddress, ethAddress } = generateAddresses(privateKeyHex);

  console.log(`Sei Address: ${seiAddress}`);
  console.log(`Ethereum Address: ${ethAddress}`);
  ```
</Accordion>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.