This is a complete walkthrough, from an empty folder to a token contract living on AERE Network that you can see in the explorer and read from any machine with curl. Nothing here is a simulation and nothing is a local testnet: every command points at the live chain.
Developers who have written a little Solidity, or none at all, but are comfortable in a terminal. If you have deployed on any EVM chain before, you can skip to step 4; the only thing that differs is the network settings.
eth_call, and decode the answer by hand.AERE Network is a public EVM Layer 1. Standard tooling works against it unmodified, which is the entire reason this course can be short. A few properties are worth knowing before you spend time here, stated plainly including the unflattering parts:
These are the network parameters. The chain ID and network ID below were both confirmed live against the two public endpoints while writing this page.
| Field | Value |
|---|---|
| Network name | AERE Network |
| Chain ID | 2800 (hex 0xAF0) |
| Currency symbol | AERE, 18 decimals |
| RPC endpoint | https://rpc.aere.network |
| Second RPC endpoint | https://rpc2.aere.network |
| Explorer | https://explorer.aere.network |
| Fee model | EIP-1559, and EIP-155 replay protection |
If your wallet supports EIP-3085, an app can request the network in one call. This is the exact object:
{
"chainId": "0xAF0",
"chainName": "AERE Network",
"nativeCurrency": { "name": "AERE", "symbol": "AERE", "decimals": 18 },
"rpcUrls": ["https://rpc.aere.network"],
"blockExplorerUrls": ["https://explorer.aere.network"]
}
From a page, that becomes:
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: "0xAF0",
chainName: "AERE Network",
nativeCurrency: { name: "AERE", symbol: "AERE", decimals: 18 },
rpcUrls: ["https://rpc.aere.network"],
blockExplorerUrls: ["https://explorer.aere.network"]
}]
});
rpc and rpc2. Be aware of what that does and does not buy you: both run the same client build, confirmed by asking each for web3_clientVersion and getting an identical string. So they give you a second endpoint, not a second independent implementation. A second client exists and is being brought up, but it has no public read endpoint today, so every check on this page is a check against one implementation.A wallet showing "AERE Network" proves only that someone typed that name. Ask the endpoint directly. Run these four calls before you trust anything else in this course.
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'
# => {"jsonrpc":"2.0","id":1,"result":"0xaf0"}
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"net_version","params":[]}'
# => {"jsonrpc":"2.0","id":1,"result":"2800"}
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'
# => a hex height that is higher every time you run it
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_syncing","params":[]}'
# => false, meaning the endpoint is at the head, not catching up
Snapshot, so you can sanity-check the order of magnitude. At 2026-08-10T11:16:16Z the head was block 13,212,316 (0xc99a9c). This is a photograph of a moving number: it climbs at about 1.8 blocks per second, so by the time you read this it is much larger. If your endpoint returns something far below this, you are not at the head.
The QBFT method is exposed publicly, so the claim "ten validators" is one you can check rather than take:
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"qbft_getValidatorsByBlockNumber","params":["latest"]}'
It returns an array of addresses. Count them: ten (nine when measured on 2026-08-10, ten since 2026-09-11). Quorum is seven of ten and f=3.
Take two headers 20,000 blocks apart and divide the timestamp difference by 20,000. Measured on 2026-08-10 over five disjoint windows of 20,000 blocks ending at height 13,211,684, every window landed between 0.550 and 0.557 seconds per block, mean 0.557 s. That is the realised interval, which is the only one you can check from outside; it is not a configured target, and it moves a little from window to window. Reproduce it with any two heights you like:
RPC=https://rpc.aere.network
rpc () { curl -s -X POST $RPC -H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":$2}"; }
H=$(rpc eth_blockNumber '[]' | sed 's/.*"result":"//;s/".*//')
NOW=$((H)); OLD=$((NOW - 20000))
ts () { rpc eth_getBlockByNumber "[\"$(printf '0x%x' $1)\",false]" \
| sed 's/.*"timestamp":"//;s/".*//'; }
T1=$(ts $OLD); T2=$(ts $NOW)
awk -v a=$((T1)) -v b=$((T2)) \
'BEGIN{printf "%.4f s per block\n",(b-a)/20000}'
# => 0.5507 s per block, on the window this was written against
The division is done with awk rather than bc on purpose: bc is missing from several common shells, including Git Bash on Windows, where it fails with an empty result rather than an error.
This is the loop behind the "close to empty" statement above. It counts transactions over the last 200 blocks, and it reuses the rpc helper defined in the previous block, so run that one first in the same shell:
H=$(rpc eth_blockNumber '[]' | sed 's/.*"result":"//;s/".*//'); N=$((H))
TX=0; EMPTY=0
for i in $(seq 0 199); do
R=$(rpc eth_getBlockByNumber "[\"$(printf '0x%x' $((N-i)))\",false]")
case "$R" in
*'"transactions":[]'*) EMPTY=$((EMPTY+1)) ;;
*'"transactions":['*)
TX=$((TX + $(printf '%s\n' "$R" | sed 's/.*"transactions":\[//;s/\].*//' \
| tr ',' '\n' | wc -l))) ;;
*) echo "not a block at height $((N-i)), stopping rather than guessing:"
printf '%s\n' "$R" | head -c 200; exit 1 ;;
esac
done
echo "$TX transactions, $EMPTY empty blocks, out of 200"
# => 3 transactions, 197 empty blocks, out of 200
# (2026-08-10, head 13,213,542)
printf '%s' without a trailing newline makes wc -l return 0 for a block with one transaction, silently undercounting; and inside a case pattern the brackets of [] must be quoted or the shell reads them as a character class.Blocks are nearly all empty, so a producer earns no fees from them. If it also earns no block reward, its balance does not move at all. Pick any validator address the previous call returned and read its balance at two heights a few hundred blocks apart. Stay within a few hundred blocks: the public endpoint serves recent state, and a request far behind the head fails rather than answering.
V=0x... # any address from the validator list above
H=$(rpc eth_blockNumber '[]' | sed 's/.*"result":"//;s/".*//'); N=$((H))
for B in $((N-450)) $((N-3)); do
rpc eth_getBalance "[\"$V\",\"$(printf '0x%x' $B)\"]"; echo
done
# => the same value twice. Measured 2026-08-10 on three of the nine
# validators across 447 blocks: not one wei of change on any of them.
This shows that no new coin was paid to those producers over that window. It is not a proof about total supply, which no single RPC call returns, and it cannot see coin created anywhere other than the addresses you check.
Deployment is a transaction, so it needs an account with a balance. Read yours with:
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["0xYourAddress","latest"]}'
Fees are paid in AERE under EIP-1559. The base fee measured on 2026-08-10 was exactly 1 Gwei, which you can read for yourself from any header:
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["latest",false]}'
# look at the baseFeePerGas field: 0x3b9aca00 = 1,000,000,000 wei = 1 Gwei
.env to .gitignore before you write the key into it. Treat any key that has ever been pasted into a chat, an issue, or a screenshot as public forever.
# .env - never commit this file
PRIVATE_KEY=0x...
RPC_URL=https://rpc.aere.network
Below is a complete ERC-20, written out rather than imported, so nothing about it is hidden behind a dependency. It implements the full standard interface: the three metadata getters, totalSupply, balanceOf, transfer, approve, allowance, transferFrom, and both events. Save it as CourseToken.sol.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title CourseToken
/// @notice A minimal, complete ERC-20. The entire supply is minted to the
/// deployer in the constructor, and there is no mint function
/// afterwards, so the supply is fixed the moment it is deployed.
contract CourseToken {
string public name;
string public symbol;
uint8 public constant decimals = 18;
uint256 public totalSupply;
// Public mappings generate the getters the standard requires:
// balanceOf(address) and allowance(address,address).
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
error InsufficientBalance(uint256 available, uint256 required);
error InsufficientAllowance(uint256 available, uint256 required);
error ZeroAddress();
/// @param _initialSupply whole tokens, not wei. 1000000 means one million
/// tokens, and the constructor scales it by 10**18 for you.
constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
totalSupply = _initialSupply * 10 ** decimals;
balanceOf[msg.sender] = totalSupply;
// Minting is a transfer from the zero address. Indexers rely on this
// event to see the supply appear; a token that skips it looks empty.
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address to, uint256 value) external returns (bool) {
_transfer(msg.sender, to, value);
return true;
}
function approve(address spender, uint256 value) external returns (bool) {
if (spender == address(0)) revert ZeroAddress();
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transferFrom(address from, address to, uint256 value) external returns (bool) {
uint256 allowed = allowance[from][msg.sender];
// An allowance of max uint256 is treated as infinite and is not
// decremented. This is the common convention and it saves a storage
// write on every pull payment.
if (allowed != type(uint256).max) {
if (allowed < value) revert InsufficientAllowance(allowed, value);
unchecked { allowance[from][msg.sender] = allowed - value; }
}
_transfer(from, to, value);
return true;
}
function _transfer(address from, address to, uint256 value) internal {
if (to == address(0)) revert ZeroAddress();
uint256 bal = balanceOf[from];
if (bal < value) revert InsufficientBalance(bal, value);
// Safe to skip the overflow checks: the subtraction is guarded above,
// and no balance can exceed totalSupply, which is fixed.
unchecked {
balanceOf[from] = bal - value;
balanceOf[to] += value;
}
emit Transfer(from, to, value);
}
}
decimals is display metadata only. The chain stores integers; a balance of one token with 18 decimals is stored as 1000000000000000000. Second, the well-known approve race is a property of the standard itself, not of this implementation: setting a non-zero allowance over another non-zero allowance lets a watching spender use both. If that matters for your use, have callers set the allowance to zero first.mkdir course-erc20 && cd course-erc20
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox dotenv
npx hardhat init # choose a JavaScript project
This walkthrough is written against Hardhat 2.x with hardhat-toolbox, and the config below is the JavaScript CommonJS form that version expects. If npx hardhat init offers you a different set of choices, you have a newer major version, whose config format and plugin names differ; either pin hardhat@2 or follow that version's own guide. The Foundry route in step 6 does not have this problem.
Put CourseToken.sol in contracts/, then write hardhat.config.js:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: {
version: "0.8.24",
settings: { optimizer: { enabled: true, runs: 200 } }
},
networks: {
aere: {
url: process.env.RPC_URL || "https://rpc.aere.network",
chainId: 2800,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
}
}
};
Then scripts/deploy.js:
const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("deployer :", deployer.address);
console.log("balance :", ethers.formatEther(
await ethers.provider.getBalance(deployer.address)), "AERE");
const token = await ethers.deployContract(
"CourseToken", ["Course Token", "CRS", 1000000n]);
await token.waitForDeployment();
console.log("address :", await token.getAddress());
console.log("tx hash :", token.deploymentTransaction().hash);
}
main().catch((e) => { console.error(e); process.exit(1); });
Compile and deploy:
npx hardhat compile
npx hardhat run scripts/deploy.js --network aere
Keep the printed address and transaction hash. You need both in steps 7 and 8.
Same contract, same chain, no Node.js involved. If you already did step 5, this is optional; running both simply gives you two tokens.
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init course-erc20-forge && cd course-erc20-forge
# put CourseToken.sol in src/, remove the sample Counter files
forge build
Add the endpoint to foundry.toml so you can refer to it by name:
[profile.default]
src = "src"
out = "out"
optimizer = true
optimizer_runs = 200
solc_version = "0.8.24"
[rpc_endpoints]
aere = "https://rpc.aere.network"
Deploy. Constructor arguments are positional, in declaration order, and the supply is in whole tokens:
source .env
forge create src/CourseToken.sol:CourseToken \
--rpc-url aere \
--private-key $PRIVATE_KEY \
--constructor-args "Course Token" "CRS" 1000000 \
--broadcast
On Foundry versions before the --broadcast flag existed, drop it; forge create broadcast by default there. Check with forge create --help if you are unsure which you have.
Foundry prints Deployer, Deployed to and Transaction hash. Move a few tokens and read the result back:
# send 25 tokens (25 * 10**18 base units) to another address
cast send $TOKEN "transfer(address,uint256)" $RECIPIENT 25000000000000000000 \
--rpc-url aere --private-key $PRIVATE_KEY
# read the recipient balance back
cast call $TOKEN "balanceOf(address)(uint256)" $RECIPIENT --rpc-url aere
The explorer follows the EIP-3091 URL scheme, so the links are predictable and you can build them yourself:
| What | URL |
|---|---|
| Your contract | https://explorer.aere.network/address/<contract address> |
| The deployment transaction | https://explorer.aere.network/tx/<tx hash> |
| The block it landed in | https://explorer.aere.network/block/<block number> |
Before you open a browser, confirm from the terminal that code is genuinely stored at the address. An address with no contract returns 0x:
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0xYourContract","latest"]}'
# => a long hex string. If it is exactly "0x", nothing is deployed there.
Also pull the receipt, which is where the deployment is proven rather than assumed. Check status is 0x1 and that contractAddress matches what your tool printed:
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["0xYourTxHash"]}'
Verification means one specific thing: proving that the bytecode running at an address is what the source you are showing people actually compiles to. Be clear about what is available here, because it differs from what you may be used to.
eth_getCode returns. This is the same method used for the core contracts published at verified-contracts.json, under the schema name aere-source-verification/1.A solc output ends with a CBOR metadata blob whose last two bytes hold its own length. Splitting that off separates executable code from metadata, which is what lets you tell a full match from a partial one. Save this as verify.js and run it with Node 18 or newer:
// verify.js - compare on-chain runtime code against your local build
const fs = require("fs");
const RPC = "https://rpc.aere.network";
const ARTIFACT = "artifacts/contracts/CourseToken.sol/CourseToken.json";
const address = process.argv[2];
if (!address) { console.error("usage: node verify.js 0xContract"); process.exit(1); }
const strip = (h) => (h || "").toLowerCase().replace(/^0x/, "");
// Last 2 bytes = length of the trailing CBOR metadata section.
function split(hex) {
if (hex.length < 8) return { code: hex, meta: "" };
const metaLen = parseInt(hex.slice(-4), 16) * 2 + 4;
if (metaLen > hex.length) return { code: hex, meta: "" };
return { code: hex.slice(0, hex.length - metaLen), meta: hex.slice(-metaLen) };
}
(async () => {
const r = await fetch(RPC, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1,
method: "eth_getCode", params: [address, "latest"] })
}).then((x) => x.json());
if (!r.result || r.result === "0x") { console.log("NOTHING DEPLOYED"); return; }
const onchain = split(strip(r.result));
const local = split(strip(JSON.parse(fs.readFileSync(ARTIFACT, "utf8")).deployedBytecode));
if (onchain.code === local.code && onchain.meta === local.meta) {
console.log("FULL MATCH, executable code and metadata hash are identical");
} else if (onchain.code === local.code) {
console.log("PARTIAL MATCH, code identical, metadata differs " +
"(usually a different comment, path, or compiler patch build)");
} else {
console.log("NO MATCH, this is not the source of that contract");
}
})();
node verify.js 0xYourContract
# => FULL MATCH, executable code and metadata hash are identical
A check that has never returned a negative result is not evidence, because you cannot tell it apart from a check that always says yes. Do both of these before you believe the green line above:
0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8. It must print NO MATCH.npx hardhat compile, and rerun against your deployed address. It must stop printing FULL MATCH.If either of those still prints FULL MATCH, your script is comparing nothing and the earlier result meant nothing either.
A partial match is not a failure. It means the executable code is byte-identical and only the metadata hash differs, which happens when a comment, a file path, or the exact compiler patch version changed. The code that runs is the same code.
immutable variables and no linked libraries. Those get their values written into the runtime code at construction time, so the on-chain bytes legitimately differ from the compiler's output and this script calls a correct source NO MATCH. The published schema handles that by masking those positions before comparing; the script above deliberately does not, because masking is the part that is easy to get wrong in a way that turns every answer green. CourseToken has neither, which is why it is safe to learn on.Every library eventually sends the same JSON-RPC request. Doing it by hand once means you can debug any of them later. A call is the 4-byte function selector, which is the first four bytes of the keccak-256 hash of the signature, followed by 32-byte arguments.
| Function | Selector | Returns |
|---|---|---|
name() | 0x06fdde03 | ABI-encoded string |
symbol() | 0x95d89b41 | ABI-encoded string |
decimals() | 0x313ce567 | uint8 |
totalSupply() | 0x18160ddd | uint256 |
balanceOf(address) | 0x70a08231 | uint256 |
allowance(address,address) | 0xdd62ed3e | uint256 |
You can practise on an ERC-20 that is already deployed, before you own one. The wrapped-coin contract works for this; it is used here only as a target that exists.
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
"to":"0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8",
"data":"0x313ce567"},"latest"]}'
# => 0x0000000000000000000000000000000000000000000000000000000000000012
# 0x12 = 18 decimals
Now name(), which returns a dynamic string and so looks more complicated than it is:
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
"to":"0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8",
"data":"0x06fdde03"},"latest"]}'
# => the answer is three 32-byte words, run together on one line:
# ...0020 offset, the string data starts 32 bytes in
# ...000c length, 12 bytes
# 577261707065642041455245 then zero padding to 32 bytes
#
# 57 72 61 70 70 65 64 = ASCII "Wrapped", 20 = a space,
# 41 45 52 45 = "AERE", so the 12 bytes decode to "Wrapped AERE"
Concatenate the selector with the address left-padded to 32 bytes: drop the 0x, then prefix 24 zeros to the 40 hex characters of the address. Put your own address in below; the 24 zeros are the padding, not part of it.
SEL=0x70a08231
ADDR=000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
# <--- 24 zeros ---><------ your 40 hex characters ------>
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_call\",\"params\":[{
\"to\":\"0xYourContract\",\"data\":\"${SEL}${ADDR}\"},\"latest\"]}"
The 32-byte answer is base units. Divide by 1018 for display:
# decode a uint256 answer in the shell
python3 -c "print(int('0x1cba1e085d0f0000', 16) / 10**18)"
The equivalents in the two toolchains, for when you no longer want to do it by hand:
# Foundry
cast call $TOKEN "balanceOf(address)(uint256)" $ADDR --rpc-url https://rpc.aere.network
# ethers v6
const t = new ethers.Contract($TOKEN, ["function balanceOf(address) view returns (uint256)"],
new ethers.JsonRpcProvider("https://rpc.aere.network"));
console.log(ethers.formatUnits(await t.balanceOf($ADDR), 18));
The same eth_call mechanism reaches the native post-quantum precompiles. Be precise about what the example below is and is not: it is SHAKE256, a hash, one of the building blocks the signature schemes are built on. It is not a signature verification, and running it proves nothing about signatures. It is the one we start with because it is the one you can check independently, against a published test vector, without holding any key.
SHAKE256 is an extendable-output function, so you ask it for a length. The precompile lives at 0x0AE5, and its input is the requested output length as a 32-byte big-endian number followed by the data. Here that is 32 bytes of output over the three bytes 616263, which is ASCII abc:
curl -s -X POST https://rpc.aere.network \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
"to":"0x0000000000000000000000000000000000000ae5",
"data":"0x0000000000000000000000000000000000000000000000000000000000000020616263"},"latest"]}'
# => 0x483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739
That value is the FIPS-202 known-answer test for SHAKE256 of "abc". Check it against your own machine rather than against us, in one line:
python3 -c "import hashlib; print(hashlib.shake_256(b'abc').hexdigest(32))"
# => 483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739
# byte for byte what the chain returned
Then change the requested length from 0x20 to 0x11, that is from 32 to 17. You get 0x483366601360a8771c6863080cc4114d8d, exactly the first 17 bytes of the same answer, which is what an extendable-output function should do and shows the length field is genuinely driving the output rather than being ignored.
eth_getCode is the wrong tool here: it returns 0x for every precompile, because they live in the client rather than in state, and an address with nothing at it returns 0x too. So the empty answer tells you nothing, and presence has to be shown by behaviour, with a control that can fail. Malformed input is enough to see the difference:
# a precompile: it parses the input, rejects it, and answers a 32-byte zero
curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
"to":"0x0000000000000000000000000000000000000ae1",
"data":"0xdeadbeef"},"latest"]}'
# => 0x0000000000000000000000000000000000000000000000000000000000000000
# NEGATIVE CONTROL, an address with no precompile behind it: nothing at all
curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
"to":"0x0000000000000000000000000000000000000ae6",
"data":"0xdeadbeef"},"latest"]}'
# => 0x
Measured 2026-08-10. Two different answers to the same malformed input is the whole point: without the second call, the first one proves nothing, because you would have no idea what a miss looks like. Note also that the height at which these were switched on is not something you can re-derive from the public endpoint, because it does not serve historical state that far back, so we leave the number off this page rather than print one you cannot check.
Run this block from any machine. It confirms the chain identity, the validator set, and your own contract, without trusting anything written above.
RPC=https://rpc.aere.network
TOKEN=0xYourContract
ME=0xYourAddress
call () { curl -s -X POST $RPC -H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":$2}"; echo; }
# 1. the chain is 2800, on both public endpoints
call eth_chainId '[]' # 0xaf0
call net_version '[]' # "2800"
# 2. the endpoint is at the head, not replaying an old view
call eth_syncing '[]' # false
call eth_blockNumber '[]' # above 0xc99a9c (13,212,316)
# 3. ten validators, counted rather than claimed
call qbft_getValidatorsByBlockNumber '["latest"]'
# 4. your contract has code, and its receipt succeeded
call eth_getCode "[\"$TOKEN\",\"latest\"]" # not "0x"
# 5. your token answers the standard interface
call eth_call "[{\"to\":\"$TOKEN\",\"data\":\"0x95d89b41\"},\"latest\"]" # symbol()
call eth_call "[{\"to\":\"$TOKEN\",\"data\":\"0x313ce567\"},\"latest\"]" # decimals() = 0x12
call eth_call "[{\"to\":\"$TOKEN\",\"data\":\"0x18160ddd\"},\"latest\"]" # totalSupply()
# 6. your balance inside your own token
call eth_call "[{\"to\":\"$TOKEN\",\"data\":\"0x70a08231000000000000000000000000${ME#0x}\"},\"latest\"]"
# 7. and the same answers from the second public endpoint
RPC=https://rpc2.aere.network
call eth_call "[{\"to\":\"$TOKEN\",\"data\":\"0x18160ddd\"},\"latest\"]"
status: 0x1, viewable in the explorer.curl.
Measured 2026-08-10 against https://rpc.aere.network. Fixed values on this page, the chain ID, the function selectors, the precompile address and its known-answer test, do not move. Moving values, the height, the base fee, the block interval and the transaction count, are labelled as snapshots with the call that reproduces them. The validator set was nine on that date, counted from qbft_getValidatorsByBlockNumber, and all of them are operated by the Foundation. The contract in step 4 was compiled with solc 0.8.26 and executed against the live chain, including its failure cases, before this page was published. If a command here disagrees with your terminal, your terminal is the record, and we would like to hear about it.