Pre-launch. The protocol is not publicly live yet. This documentation describes the contracts as implemented; deployed addresses are published at launch.

Deploy a Token

Mine a vanity salt, then deploy a token + bonding curve pair through the Factory. Examples use ethers v6.

Prerequisites

  • Native asset for the deployment fee — Base 0.06 ETH, BSC 0.18 BNB, Polygon 1,440 POL, Robinhood 0.06 ETH, Gnosis 144 xDAI
  • An ethers v6 provider and signer
  • The Factory and token master addresses for your chain
Mine against your own address

The factory does not pass your salt to CREATE2 — it passes keccak256(salt ‖ msg.sender). A salt mined for one deployer produces a different address for anyone else, so you must include your deploying address when mining. This is what makes a mined salt useless if copied out of the mempool.

Step 1 — Build the salt

The 32-byte salt is three fields: an 8-byte anchor block, the first 8 bytes of that block's hash, and a 16-byte nonce you mine.

import { ethers } from "ethers";

// EIP-1167 minimal proxy initcode for the token master
function proxyInitCodeHash(tokenMaster) {
    const initcode = ethers.concat([
        "0x3d602d80600a3d3981f3363d3d373d3d3d363d73",
        tokenMaster,
        "0x5af43d82803e903d91602b57fd5bf3"
    ]);
    return ethers.keccak256(initcode);
}

async function mineSalt(provider, factory, tokenMaster, deployer) {
    const anchorBlock = await provider.getBlockNumber();
    const block = await provider.getBlock(anchorBlock);

    // [0:8] anchor block, big-endian
    const anchorField = ethers.zeroPadValue(ethers.toBeHex(anchorBlock), 8);
    // [8:16] first 8 bytes of the blockhash
    const hashField = ethers.dataSlice(block.hash, 0, 8);

    const initCodeHash = proxyInitCodeHash(tokenMaster);
    const deployerWord = ethers.zeroPadValue(deployer, 32);

    for (let nonce = 0n; ; nonce++) {
        // [16:32] mined nonce
        const nonceField = ethers.zeroPadValue(ethers.toBeHex(nonce), 16);
        const salt = ethers.concat([anchorField, hashField, nonceField]);

        // The factory domain-separates the salt by msg.sender
        const effectiveSalt = ethers.keccak256(ethers.concat([salt, deployerWord]));
        const predicted = ethers.getCreate2Address(factory, effectiveSalt, initCodeHash);

        if (predicted.toLowerCase().startsWith("0x24")) {
            return { salt, predicted, anchorBlock };
        }
    }
}
from eth_utils import keccak, to_checksum_address
from web3 import Web3

# EIP-1167 minimal proxy initcode for the token master
def proxy_initcode_hash(token_master):
    initcode = (
        bytes.fromhex("3d602d80600a3d3981f3363d3d373d3d3d363d73")
        + bytes.fromhex(token_master[2:])
        + bytes.fromhex("5af43d82803e903d91602b57fd5bf3")
    )
    return keccak(initcode)

def mine_salt(w3, factory, token_master, deployer):
    anchor_block = w3.eth.block_number
    block = w3.eth.get_block(anchor_block)

    # [0:8] anchor block, big-endian
    anchor_field = anchor_block.to_bytes(8, "big")
    # [8:16] first 8 bytes of the blockhash
    hash_field = bytes(block["hash"])[:8]

    initcode_hash = proxy_initcode_hash(token_master)
    deployer_word = bytes(12) + bytes.fromhex(deployer[2:])
    factory_bytes = bytes.fromhex(factory[2:])

    nonce = 0
    while True:
        # [16:32] mined nonce
        salt = anchor_field + hash_field + nonce.to_bytes(16, "big")

        # The factory domain-separates the salt by msg.sender
        effective_salt = keccak(salt + deployer_word)
        predicted = to_checksum_address(
            keccak(b"\xff" + factory_bytes + effective_salt + initcode_hash)[12:]
        )

        if predicted.lower().startswith("0x24"):
            return salt, predicted, anchor_block
        nonce += 1

One byte of prefix is a 1-in-256 hit rate — a few hundred iterations on average, effectively instant.

Step 2 — Pack socials (optional)

The fourth argument is 32 opaque bytes stored in TokenSocials. The contract takes no view on their meaning; encoding is an application-layer convention. Pass ethers.ZeroHash to skip it and set socials later.

Step 3 — Deploy

const FACTORY_ABI = [
  "function deploy_pair(string,string,bytes32,bytes32) payable returns (address,address)",
  "function get_deployment_fee() view returns (uint256)",
  "event PairDeployed(address indexed token, address indexed curve, address indexed deployer, string name, string symbol, bytes32 packed_socials)"
];

const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, signer);
const fee = await factory.get_deployment_fee();

const { salt, predicted } = await mineSalt(
    provider, FACTORY_ADDRESS, TOKEN_MASTER, await signer.getAddress()
);

const tx = await factory.deploy_pair(
    "My Token", "MTK", salt, ethers.ZeroHash, { value: fee }
);
const receipt = await tx.wait();

// Recover the deployed addresses from the log
const ev = receipt.logs
    .map(l => { try { return factory.interface.parseLog(l); } catch { return null; } })
    .find(e => e?.name === "PairDeployed");

const [token, curve] = [ev.args.token, ev.args.curve];
console.log(token === predicted);  // true
w3 = Web3(Web3.HTTPProvider(RPC_URL))
acct = w3.eth.account.from_key(PRIVATE_KEY)

factory = w3.eth.contract(address=FACTORY_ADDRESS, abi=FACTORY_ABI)
fee = factory.functions.get_deployment_fee().call()

salt, predicted, _ = mine_salt(w3, FACTORY_ADDRESS, TOKEN_MASTER, acct.address)

tx = factory.functions.deploy_pair(
    "My Token", "MTK", salt, bytes(32)
).build_transaction({
    "from": acct.address,
    "value": fee,
    "nonce": w3.eth.get_transaction_count(acct.address),
})

signed = acct.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

# Recover the deployed addresses from the log
ev = factory.events.PairDeployed().process_receipt(receipt)[0]

token, curve = ev["args"]["token"], ev["args"]["curve"]
print(token == predicted)  # True
Mine and broadcast in one go

The salt is only valid for INCLUSION_WINDOW blocks after its anchor — Base 10 blocks, BSC 44 blocks, Polygon 12 blocks, Robinhood 200 blocks, Gnosis 5 blocks. On short-window chains, mine immediately before broadcasting and use a gas price that will land promptly. If you miss it the transaction reverts with Too late; re-mine against a fresh anchor and resubmit.

Step 4 — Seed the curve (optional)

The deployment fee is forwarded to the bonding curve as its opening balance. You are not obliged to buy, but note that the deployer can never sellsell() and refund_claim() both reject the deployer address. Anything you buy on your own curve is locked until graduation.

Common reverts

RevertCause
Protocol token not yet deployedThe chain's protocol pair has not been created — the factory is not open for public deployment yet
Incorrect feemsg.value must equal the fee exactly; read it from get_deployment_fee()
Vanity missMined against the raw salt instead of keccak256(salt ‖ msg.sender), or against the wrong deployer address
Blockhash mismatchBytes [8:16] do not match the anchor block's hash prefix
Too latePast the inclusion window — re-mine
Anchor not minedAnchor block is the current block or later
Blockhash unavailableAnchor is more than 256 blocks back — the transaction sat too long