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-skillThe 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
-
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 -
Pick a name: uppercase
A–Z, digits0–9and dash, at most 20 characters for the English collection. -
Check availability, and stop if
existscomes backtrue: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)" }collectionandallowed_name_lengthcome from the live factory schema, andexistsis an on-chainhas_communitycall.allowed_charsdescribes the English collection.
Create
node scripts/superhero-token-create.mjs create "MYTOKEN" 0.1| Parameter | Meaning |
|---|---|
| Name | Uppercase, digits, dash. Max 20 characters. Unique within its collection |
buy_ae | AE 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
- Load the signing account from
AE_PRIVATE_KEYand connect to mainnet. - Initialise the factory at the mainnet address
ct_25cqTw85wkF5cbcozmHHUCuybnfH9WaRZXSgEcNNXG9LsCJWTNusingcontracts/CommunityFactory.aci.json. - Check the balance if
buy_aeis greater than zero. It requiresbalance >= buy_ae + 0.01, where the extra 0.01 AE is gas headroom. - Resolve the collection. It fetches
https://api.superhero.com/api/factoryand takes the first collection in the schema, which is the EnglishWORDS-ak_...one. To create in a different collection, use the SDK path below. - Check the name with
factory.has_community(collectionId, tokenName)and abort if it is taken. - 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.
- Call
create_communitywith the collection id, the name, the scaled initial buy count,is_private: false, an emptymeta_infomap, and the buffered amount asamount. - 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
| Output | Exit | Cause |
|---|---|---|
AE_PRIVATE_KEY environment variable is not set... | 1 | No signing key in the environment. Required by create, check and info alike |
Usage: node scripts/superhero-token-create.mjs create <name> [buy_ae] | 1 | create called with no name |
{"error":"Insufficient balance","balance_ae":"…","required_ae":"…"} | 1 | buy_ae greater than zero and wallet balance below buy_ae + 0.01 |
ERROR: Could not get collection from Superhero factory API | 1 | GET /api/factory returned no usable collections object. Usually the API being down |
{"error":"Token already exists","collection":"…","name":"…"} | 1 | has_community returned true |
Usage: node scripts/superhero-token-create.mjs check <name> | 1 | check called with no name |
Usage: node scripts/superhero-token-create.mjs info <sale_address> | 1 | info called with no address |
API error: 404 | 1 | info called with an address the token API does not know |
ERROR: <message> | 1 | Anything 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
- Create a Token (Dashboard): the same deployment through the web form, with live price estimates
- Curve Parameters & Units: why
initialBuyCountis scaled by 10^18 and whatsupported_decimals()is for - Collections & Naming Rules: how to pick a collection deliberately instead of taking the first one
- Buying & Selling: the trading script, which follows the same conventions
- Agent CLI reference: every script in the skill
- æternity JavaScript SDK: contracts (opens in a new tab):
Contract.initialize, ACIs and call options such asamount