Tokens & Bonding Curves
Create a Token (CLI)

Create a Token from the Command Line

scripts/superhero-token-create.mjs in the agent skill wraps CommunityFactory.create_community in three subcommands: check, create and info. It signs with a key from your environment, resolves the collection for you, and prints one line of JSON on success.

Setup

export AE_PRIVATE_KEY=<your_secret_key>
cd /path/to/superhero-agent-skill

The script reads its ACI files from ./contracts/ relative to the current directory, so run it from the skill's root. It signs with an in-memory account built from AE_PRIVATE_KEY and talks to https://mainnet.aeternity.io; the node URL and the factory address are constants in the file.

Pre-flight

  1. Confirm you have AE for gas (about 0.001 AE for creation, plus whatever you intend to pre-buy):

    node scripts/superhero-wallet.mjs balance
  2. Pick a name: uppercase A–Z, digits 0–9 and dash, at most 20 characters for the English collection.

  3. Check availability, and stop if exists comes back true:

    node scripts/superhero-token-create.mjs check "MYTOKEN"
    {
      "collection": "WORDS-ak_...",
      "name": "MYTOKEN",
      "exists": false,
      "allowed_name_length": "20",
      "allowed_chars": "A-Z, 0-9, - (dash)"
    }

    collection and allowed_name_length come from the live factory schema, and exists is an on-chain has_community call. allowed_chars describes the English collection.

Create

node scripts/superhero-token-create.mjs create "MYTOKEN" 0.1
ParameterMeaning
NameUppercase, digits, dash. Max 20 characters. Unique within its collection
buy_aeAE to spend on an initial buy at creation. Optional, default 0

On success, one JSON object on stdout:

{
  "success": true,
  "token_name": "MYTOKEN",
  "collection": "WORDS-ak_...",
  "sale_address": "ct_...",
  "dao_address": "ct_...",
  "community_management_address": "ct_...",
  "initial_buy_ae": 0.1,
  "estimated_tokens": "131748.00",
  "tx_hash": "th_..."
}

Progress messages go to stderr, so 2>/dev/null gives you parseable JSON and nothing else.

What the script does, step by step

  1. Load the signing account from AE_PRIVATE_KEY and connect to mainnet.
  2. Initialise the factory at the mainnet address ct_25cqTw85wkF5cbcozmHHUCuybnfH9WaRZXSgEcNNXG9LsCJWTN using contracts/CommunityFactory.aci.json.
  3. Check the balance if buy_ae is greater than zero. It requires balance >= buy_ae + 0.01, where the extra 0.01 AE is gas headroom.
  4. Resolve the collection. It fetches https://api.superhero.com/api/factory and takes the first collection in the schema, which is the English WORDS-ak_... one. To create in a different collection, use the SDK path below.
  5. Check the name with factory.has_community(collectionId, tokenName) and abort if it is taken.
  6. Size the initial buy, if there is one:
    • Estimate a token count off-chain by bisecting the curve integral against your AE figure (up to 1,000 iterations, search bounds 0 to 10^12 tokens, always from supply zero). The stopping tolerance is an absolute 0.001 AE, so a 0.1 AE initial buy is sized to within about 1%.
    • Read supported_decimals() off the bonding curve and scale the count into token decimal units.
    • Ask the chain for the real price: bonding_curve.calculate_buy_price(0, count).
    • Multiply that by 1.05 and round up. This is the AE attached to the transaction.
  7. Call create_community with the collection id, the name, the scaled initial buy count, is_private: false, an empty meta_info map, and the buffered amount as amount.
  8. Print the returned (DAO, TokenSale, CommunityManagement) tuple with the transaction hash.

The 5% buffer

The amount attached to the transaction is the price plus five percent:

attached = ceil( calculate_buy_price(0, count) × 1.05 )

calculate_buy_price returns the bare curve integral; the sale contract also charges a 0.5% affiliation fee, so roughly a tenth of the buffer covers the fee. A new token is created at supply zero by definition, so there is no curve drift between quote and execution for a creation. The same buffer pattern is reused for ordinary buys, where drift is real.

The remainder comes back. create_community requires Call.value >= buy_price rather than equality, then refunds the difference in the same transaction:

let buy_price = bonding_curve_token_sale.price(initial_buy_count)
require(Call.value >= buy_price, "VALUE_HAS_TO_BE_GREATER_THAN_BUY_PRICE")
let overpaid_refund = Call.value - buy_price
if (overpaid_refund > 0) Chain.spend(Call.caller, overpaid_refund) else ()

The check is against price(initial_buy_count) on the sale, which already includes the 0.5% affiliation fee. The fee comes out of the buffer and the rest is returned, so you spend the real price plus gas.

ProtocolDAO tokens are minted on the net amount: the factory mints (Call.value - overpaid_refund) * 1000, so you are rewarded on what you spent, not on what you attached.

Inspect afterwards

node scripts/superhero-token-create.mjs info ct_<sale_address>
node scripts/superhero-trending.mjs token-info ct_<sale_address>

Both hit GET /api/tokens/{address} and print the full token record.

Timing

Creation takes 2–5 minutes to mine. Run the command in a background terminal and wait up to 10 minutes before treating it as failed. Do not resubmit in the meantime: a second transaction with the same name fails on-chain once the first one lands, and you pay gas twice.

Error paths

OutputExitCause
AE_PRIVATE_KEY environment variable is not set...1No signing key in the environment. Required by create, check and info alike
Usage: node scripts/superhero-token-create.mjs create <name> [buy_ae]1create called with no name
{"error":"Insufficient balance","balance_ae":"…","required_ae":"…"}1buy_ae greater than zero and wallet balance below buy_ae + 0.01
ERROR: Could not get collection from Superhero factory API1GET /api/factory returned no usable collections object. Usually the API being down
{"error":"Token already exists","collection":"…","name":"…"}1has_community returned true
Usage: node scripts/superhero-token-create.mjs check <name>1check called with no name
Usage: node scripts/superhero-token-create.mjs info <sale_address>1info called with no address
API error: 4041info called with an address the token API does not know
ERROR: <message>1Anything else: node unreachable, gas too low, a contract revert such as NAME_ALREADY_REGISTERED, an invalid private key

A contract revert surfaces as the last row: the SDK throws, and the script prints ERROR: with the contract's own reason string. The two to recognise are NAME_ALREADY_REGISTERED (someone claimed the name between your check and your transaction) and any message mentioning the allowed character set or name length (your name does not fit the collection you were routed to).

Without the skill scripts

The same deployment against @aeternity/aepp-sdk directly. You need CommunityFactory.aci.json and BondingCurve.aci.json, both of which are in the skill's contracts/ directory.

import { AeSdk, Node, MemoryAccount, Contract } from '@aeternity/aepp-sdk';
import BigNumber from 'bignumber.js';
import fs from 'fs';
 
const NODE_URL = 'https://mainnet.aeternity.io';
const FACTORY = 'ct_25cqTw85wkF5cbcozmHHUCuybnfH9WaRZXSgEcNNXG9LsCJWTN';
 
const aeSdk = new AeSdk({
  nodes: [{ name: 'mainnet', instance: new Node(NODE_URL) }],
  accounts: [new MemoryAccount(process.env.AE_PRIVATE_KEY)],
});
 
const factory = await Contract.initialize({
  address: FACTORY,
  aci: JSON.parse(fs.readFileSync('./CommunityFactory.aci.json', 'utf8')),
  onAccount: aeSdk,
  onNode: aeSdk.api,
});
 
// 1. Pick a collection. Read the schema from the API, or call the factory:
//    allowed_name_length(name) and allowed_name_chars(name) describe each one.
const schema = await fetch('https://api.superhero.com/api/factory').then((r) => r.json());
const collectionId = Object.values(schema.collections)[0].id;   // e.g. "WORDS-ak_..."
 
const name = 'MYTOKEN';
 
// 2. Refuse to burn gas on a name that is already taken.
if ((await factory.has_community(collectionId, name)).decodedResult) {
  throw new Error(`${name} already exists in ${collectionId}`);
}
 
// 3. Decide how many tokens to pre-buy, and ask the curve what they cost.
const curveAddress = (await factory.bonding_curve()).decodedResult;
const curve = await Contract.initialize({
  address: curveAddress,
  aci: JSON.parse(fs.readFileSync('./BondingCurve.aci.json', 'utf8')),
  onAccount: aeSdk,
  onNode: aeSdk.api,
});
 
const decimals = Number((await curve.supported_decimals()).decodedResult);
const initialBuyCount = new BigNumber(437014)                 // whole tokens
  .multipliedBy(new BigNumber(10).pow(decimals))
  .toFixed(0);
 
const price = (await curve.calculate_buy_price(0n, BigInt(initialBuyCount))).decodedResult;
const amount = new BigNumber(price.toString())
  .multipliedBy(1.05)                                          // fee + drift headroom
  .toFixed(0, BigNumber.ROUND_CEIL);
 
// 4. Deploy. meta_info is a Sophia map(string, string); an empty Map is valid.
const result = await factory.create_community(
  collectionId,
  name,
  BigInt(initialBuyCount),
  false,                                                       // is_private
  new Map(),                                                   // meta_info
  { amount },
);
 
const [dao, tokenSale, communityManagement] = result.decodedResult;
console.log({ dao, tokenSale, communityManagement, txHash: result.hash });

Pass initialBuyCount as 0n and amount as '0' to create with no pre-buy. To create in a non-English collection, substitute its id at step 1.

Factory address

Mainnet community factory: ct_25cqTw85wkF5cbcozmHHUCuybnfH9WaRZXSgEcNNXG9LsCJWTN

Resolve it from GET /api/factory in anything long-lived, so that a redeployment does not leave a hardcoded address pointing at an abandoned registry.

Related