---
title: 'Paxeer Network Accounts: Dual Address System'
sidebarTitle: 'Account Linking'
description: "How Paxeer's unified account system links EVM (0x) and Cosmos (pax1) addresses, including association methods."
keywords: ['paxeer accounts', 'dual address system', 'address association', 'EVM address', 'Cosmos address', 'wallet linking']
---

> **For AI agents:** the complete documentation index is at [llms.txt](/llms.txt). Append `.md` to any page URL for its markdown version.

Every account on Paxeer has a unique public key. This public key derives two wallet addresses that point to the same account.

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

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

These addresses share the same underlying account. Once associated, depositing funds into one address makes them accessible from the other. Until associated, they are treated as separate accounts with separate balances.

## Key points

### Before linking

- The bech32 (`pax1...`) and EVM (`0x...`) addresses are treated as **separate accounts**.
- They will have separate balances until linked.
- Cosmos tokens received by the EVM address prior to association will be held in a temporary bech32 address, which transfers to the associated address upon linking.
- Some types of transactions will **not be possible** (see table below).

### After linking

- Balances are reflected consistently across both addresses.
- Applications can query either address format seamlessly.

## Wallet association and transfer limitations

Certain actions are **not possible** before wallets are associated:

- Transfers of Cosmos-based tokens from a non-EVM wallet to an **unassociated EVM address**.
- Transfers of ERC-based tokens (ERC-20/721/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 | Provide private key directly |
| 3. Signed Message | Medium | Sign a predefined message to prove ownership *(recommended for wallets)* |
| 4. Public Key | Low | Provide a compressed public key for association |

<Info>Each method ensures the **public key** is known to the chain, enabling automatic association between the EVM-compatible and bech32 addresses. All four go through the on-chain `addr` precompile (`0x0000000000000000000000000000000000001004`) or the EVM ante handler.</Info>

## Method 1: Broadcast a transaction

When an account broadcasts a transaction, its public key is recorded on-chain and the addresses are linked automatically.

## Method 2: Direct private key association

<Danger>**Security Risk**: **High** - Requires the private key to be directly available. Exposing the private key can compromise the wallet.</Danger>

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

const PRIVATE_KEY = '<replace_with_private_key>';

const publicClient = createPublicClient({
  chain: { id: 125, rpcUrls: { default: { http: ['https://public-rpc.paxeer.app/evm/reference'] } } },
  transport: http('https://public-rpc.paxeer.app/evm/reference')
});
const client = createWalletClient({
  chain: { id: 125, rpcUrls: { default: { http: ['https://public-rpc.paxeer.app/evm/reference'] } } },
  transport: http('https://public-rpc.paxeer.app/evm/reference')
});

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** - Requires signing a specific message using the private key.</Warning>

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

const client = createWalletClient({
  chain: { id: 125, rpcUrls: { default: { http: ['https://public-rpc.paxeer.app/evm/reference'] } } },
  transport: http('https://public-rpc.paxeer.app/evm/reference')
});

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** - Uses the public key, which is less sensitive than the private key.</Info>

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

const client = createWalletClient({
  chain: { id: 125, rpcUrls: { default: { http: ['https://public-rpc.paxeer.app/evm/reference'] } } },
  transport: http('https://public-rpc.paxeer.app/evm/reference')
});

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

Resolve either side of an existing association by calling the `addr` precompile at `0x0000000000000000000000000000000000001004` over a standard `eth_call`.

### Fetch bech32 address for an EVM address

```bash
curl -X POST https://public-rpc.paxeer.app/evm/reference -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)` (inherited from the Cosmos SDK). The returned ABI-encoded string is the bech32 `pax1...` address, or the call reverts if the EVM address is not yet associated.

In TypeScript with viem:

```ts
import { createPublicClient, http, defineChain } from 'viem';

const paxeer = defineChain({
  id: 125,
  name: 'Paxeer Network',
  nativeCurrency: { name: 'PAX', symbol: 'PAX', decimals: 18 },
  rpcUrls: { default: { http: ['https://public-rpc.paxeer.app/evm/reference'] } }
});

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: paxeer, transport: http() });

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

### Fetch EVM address for a bech32 address

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

The precompile reverts if the address has never been associated - surface this as "not linked" rather than re-throwing.

## Deriving addresses from the public key

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

### Bech32 address derivation

The Cosmos address is derived from the public key using the following steps:

1. Take the **compressed** secp256k1 public key (33 bytes; first byte `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 `pax` prefix.

Example implementation:

```ts
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 derivePaxeerAddress(compressedPublicKey: Uint8Array): string {
  const digest = ripemd160(sha256(compressedPublicKey));
  return bech32.encode('pax', bech32.toWords(digest));
}
```

### EVM address derivation

The EVM-compatible address is derived as follows:

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

Example implementation:

```ts
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

- **Paxeer address**: `bech32('pax', RIPEMD160(SHA256(compressedPubKey)))` - 20 bytes, Cosmos-standard derivation.
- **EVM address**: `'0x' + keccak256(uncompressedPubKey[1:])[-20:]` - 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 from it deterministically.

## HD paths and coin types

The HD path's second parameter specifies the coin type, defined by BIP-44:

- **Ethereum (Coin Type 60)**: Wallets like MetaMask use coin type 60. The HD path is `m/44'/60'/0'/0/0`.
- **Cosmos (Coin Type 118)**: Cosmos-based wallets use coin type 118. The HD path is `m/44'/118'/0'/0/0`.

A mnemonic for coin type 60 (Ethereum) derives different keys than coin type 118 (Cosmos). Export your private key from MetaMask and import it into a Cosmos wallet to access the same account across ecosystems.

## Generating wallets

<Accordion title="TypeScript - full derivation from private key">

```typescript
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';

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 generateAddresses(privateKeyHex: string): {
  paxAddress: string;
  ethAddress: string;
} {
  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.');
  }

  const publicKey = secp256k1.getPublicKey(privateKey, true);
  const sha256Digest = sha256(publicKey);
  const ripemd160Digest = ripemd160(sha256Digest);
  const fiveBitArray = convertBits(ripemd160Digest, 8, 5, true);
  const paxAddress = bech32.encode('pax', fiveBitArray, 256);

  const publicKeyUncompressed = secp256k1.getPublicKey(privateKey, false).slice(1);
  const keccakHash = keccak_256(publicKeyUncompressed);
  const ethAddress = `0x${Buffer.from(keccakHash).slice(-20).toString('hex')}`;

  return { paxAddress, ethAddress };
}

const privateKeyHex = '907ab4bf7fc60cff';
const { paxAddress, ethAddress } = generateAddresses(privateKeyHex);

console.log(`Paxeer Address: ${paxAddress}`);
console.log(`Ethereum Address: ${ethAddress}`);
```

</Accordion>
