Bridging MetaMask to the AO Network: A Journey of Discovery
How we cracked the code to connect Ethereum wallets with Arweave's compute layer
The Challenge
Picture this: You're building a decentralized game on the AO Network - Arweave's groundbreaking compute layer. Your users have MetaMask wallets full of ETH and tokens, but AO requires Arweave wallets. Do you force them to create new wallets? Transfer funds? Remember new seed phrases?
We said no. There had to be a better way.
The Quest Begins

Code editor showing React component structure
First Attempt: The Naive Approach
// This seemed logical... but it wasn't
const result = await ao.message({
process: PROCESS_ID,
signer: metamaskSigner, // π« Nope!
});
Error after error. "Invalid signer format." "Signature mismatch." "Owner must be 512 bytes." Each error message was a breadcrumb on the trail.
The Breakthrough: Understanding Signature Types
The eureka moment came when we discovered that Arweave supports multiple signature types:
- Type 1: Native Arweave (RSA - 512 bytes)
- Type 3: Ethereum (ECDSA - 65 bytes)
The "Owner must be 512 bytes" error? It was expecting an RSA key but getting an Ethereum key!
The Solution
After days of debugging, we cracked it. Here's the magic formula:
Step 1: Extract the Ethereum Public Key
const ethSigner = new InjectedEthereumSigner(provider);
await ethSigner.setPublicKey(); // MetaMask prompts for signature
Step 2: Derive the Arweave Address
// Ethereum public key β SHA-256 β Base64url = Arweave address
const hashBuffer = await crypto.subtle.digest('SHA-256', publicKey);
const arweaveAddress = base64url(hashBuffer).slice(0, 43);
Step 3: Create the Magic Signer
const ethArweaveSigner = async (create, signatureType) => {
// The secret sauce: specify type 3!
const dataToSign = await create({
publicKey: ethSigner.publicKey,
type: 3, // π This was the missing piece!
});
const signature = await ethSigner.sign(dataToSign);
return { signature, address: arweaveAddress };
};
Step 4: Send Messages to AO
const result = await ao.message({
process: PROCESS_ID,
tags: [{ name: 'Action', value: 'Hello-From-MetaMask' }],
data: 'We did it!',
signer: ethArweaveSigner,
});
And it worked! π
The Impact
This breakthrough means:
- No New Wallets: Users keep their familiar MetaMask
- Instant Onboarding: Click, sign, and you're on AO
- Cross-Chain Identity: Same address across Ethereum and Arweave ecosystems
- Security: Private keys never leave MetaMask
Technical Deep Dive
For the curious minds, here's what's happening under the hood:
-
Public Key Extraction: MetaMask doesn't expose public keys directly. We get it by asking users to sign a message and recovering the public key from the signature.
-
Address Normalization: Arweave addresses are 43-character Base64url strings. We convert the Ethereum public key to this format using SHA-256 hashing.
-
Signature Compatibility: The
type: 3
parameter tells AO to expect an Ethereum signature (65 bytes) instead of an Arweave signature (512 bytes). -
Message Format: AO messages are actually Arweave DataItems (ANS-104 standard) that get bundled and stored permanently.
The Code That Started It All
Here's the complete working example that brought MetaMask to AO:
import { connect } from '@permaweb/aoconnect';
import { InjectedEthereumSigner } from 'arseeding-arbundles/src/signing';
import { Web3Provider } from '@ethersproject/providers';
// Initialize
const provider = new Web3Provider(window.ethereum);
const ethSigner = new InjectedEthereumSigner(provider);
await ethSigner.setPublicKey();
// Derive Arweave address
const deriveAddress = async (publicKey: Uint8Array) => {
const hash = await crypto.subtle.digest('SHA-256', publicKey);
const base64 = btoa(String.fromCharCode(...new Uint8Array(hash)));
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '').slice(0, 43);
};
const arweaveAddress = await deriveAddress(ethSigner.publicKey);
// Create AO connection
const ao = connect({ MODE: 'legacy' });
// The magic signer
const signer = async (create: any) => {
const dataToSign = await create({
publicKey: ethSigner.publicKey,
type: 3, // Ethereum signature type
});
return {
signature: await ethSigner.sign(dataToSign),
address: arweaveAddress,
};
};
// Send message!
const result = await ao.message({
process: 'YOUR_PROCESS_ID',
tags: [{ name: 'Action', value: 'Greetings' }],
data: 'Hello from the Ethereum side!',
signer,
});
console.log('π Message sent to AO:', result);
What's Next?

Code editor showing React component structure
For Developers
- Unified Auth: One wallet for all Web3 needs
- Easy Migration: Bring Ethereum users to Arweave/AO
- New Patterns: Cross-chain composability
For Users
- Simplified UX: No wallet juggling
- Familiar Tools: Keep using MetaMask
- Broader Access: Join the Arweave ecosystem instantly
Building the Future
We're not stopping here. We're building:
- React Hooks:
useAOSigner()
,useArweaveAddress()
- TypeScript SDK: Full type safety
- Multi-Wallet Support: WalletConnect, Coinbase Wallet, etc.
- Cross-Chain Messages: Ethereum events triggering AO processes
Join the Revolution

Code editor showing React component structure
Want to implement this in your project? Check out our open-source demo and documentation.
The Community
Special thanks to the Arweave and AO communities for building the infrastructure that made this possible. To the arseeding-arbundles
team for Ethereum signing support. And to every developer who's ever stared at an error message and thought, "There must be a way."
There always is. You just have to find it.
Have questions? Found improvements? Reach out on Twitter or open an issue on GitHub.
Happy building, and welcome to the cross-chain future! π