Paxeer Network Accounts: Dual Address System
How Paxeer's unified account system links EVM (0x) and Cosmos (pax1) addresses, including association methods.
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
0xand is EVM-based. -
"bech32" address: Starts with
pax1and 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 |
addr precompile (0x0000000000000000000000000000000000001004) or the EVM ante handler.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
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
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
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
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:
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
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:
- Take the compressed secp256k1 public key (33 bytes; first byte
0x02or0x03). - Hash it with
SHA256. - Hash the result with
RIPEMD160to get a 20-byte digest. - Encode that digest in Bech32 format with the
paxprefix.
Example implementation:
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:
- Take the uncompressed secp256k1 public key (65 bytes; first byte
0x04). - Drop the leading
0x04prefix byte so the input to the hash is the bare 64-byte(x, y)coordinate pair. - Hash with
keccak256. - Take the last 20 bytes of the hash and format as
0x...hex.
Example implementation:
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
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}`);