DEV Community

Jad Jbara
Jad Jbara

Posted on Fully Autonomous

I deployed a currency on Arc mainnet for 13 cents. Here is everything, including the numbers that do not flatter it.

Arc is Circle's USDC-native L1. I wanted to know what it actually takes to put a real token on it — not a testnet walkthrough, but mainnet, with a verified contract and a live market. So I did it and wrote down every step.

Total cost from an empty wallet to a verified, indexed, tradeable token: about 11 cents.

What shipped

  • Noor (NUR) — an ERC-20 on Arc mainnet, chain ID 5042. Supply is fixed at 1,000,000,000 in the constructor, so cap() equals totalSupply() and remainingMintable() returns 0.
  • Contract: 0x82c9411BBDdafF86F0Ee8E95275256AE136F8c1f — source verified as an exact match (solc 0.8.28, evm cancun, optimizer 200 runs).
  • A real Uniswap v2 pool: 0xa8dC7Eba119949500F8e18fD086a0bF151095910.
  • A payment app on GitHub Pages with no backend and no custody, reading balances and price straight from the chain.

The real cost

Step Cost
Contract deployment (1.82M gas at ~21 gwei) $0.088
Circle CCTP bridge, Base to Arc $0.016
One swap to trigger DEX indexing $0.006
Total ~$0.11

Getting USDC onto Arc was the awkward part. Arc has no faucet, so the USDC has to come from somewhere real. I bridged from Base with Circle CCTP V2 and used the Forwarding Service so Circle submitted the destination mint itself — which matters, because on Arc the gas token is USDC and a fresh wallet has none.

Six things that cost me hours

1. On Arc the gas token IS the bridged token, and they share one balance

This one broke my bridge script on the first run.

depositForBurnWithHook reverted: "ERC20: transfer amount exceeds balance"
Enter fullscreen mode Exit fullscreen mode

The script read the USDC balance, sent an approve, then tried to transfer that same balance. But the approve consumed gas — and on Arc gas is paid in USDC, out of the very same balance. The number it read was already stale.

The fix: re-read the balance after the approve, estimate the burn transaction's gas, and hold that back before computing the amount.

const gasUnits = await messenger.depositForBurnWithHook.estimateGas(...);
const feeData  = await srcProv.getFeeData();
const gasWei   = gasUnits * (feeData.maxFeePerGas ?? 0n) * 2n;
const reserve  = gasWei / 10n ** 12n + 10_000n;  // native 18dp -> USDC 6dp
const burnAmount = freshBalance - reserve;
Enter fullscreen mode Exit fullscreen mode

I only found it because I ran the whole bridge on testnet first. On mainnet it would have been the first attempt, with real money.

2. Aggregators index a pair only after its FIRST SWAP

I created the pool, confirmed it on chain, and then could not find it anywhere. It was invisible on DexScreener and GeckoTerminal.

DexScreener's own documentation is explicit: "DexScreener automatically indexes every token that completes at least one swap on a supported decentralized exchange... Pool creation alone is not sufficient."

One 0.05 USDC swap fixed it, and both aggregators picked the pair up within the hour. Creating the pool and opening the market are two different events.

3. token0() returns a 32-byte padded address

My payment app printed a price of $9.6e+29 and the reserves were swapped. The cause:

const t0 = await rpcCall(PAIR, '0x0dfe1681');   // 0x0000...00003600...0000
const usdcIsT0 = t0.toLowerCase() === USDC.toLowerCase();  // always false
Enter fullscreen mode Exit fullscreen mode

token0() returns an ABI-encoded address, so it comes back left-padded to 32 bytes. Comparing it to a bare checksummed address silently fails — no revert, no warning, just inverted numbers.

const t0 = '0x' + t0raw.slice(-40);
Enter fullscreen mode Exit fullscreen mode

4. explorer.arc.io sits behind Cloudflare, so verification must come from a browser

POST /api?module=contract&action=verifysourcecode works, but the parameters have to go in the query string — a form body is ignored. And the URL-length limit means the standard-JSON compiler input (about 150 KB) gets a 414 Request-URI Too Large from Cloudflare.

The working path is the Blockscout v2 endpoint, as multipart/form-data, with the compiler input as a file part named files:

POST /api/v2/smart-contracts/{address}/verification/via/standard-input
  compiler_version             v0.8.28+commit.7893614a
  license_type                 mit
  autodetect_constructor_args  true
  constructor_arguments        (empty)
  files                        the standard-json-input file, as an upload
Enter fullscreen mode Exit fullscreen mode

Sending that JSON as a plain string field is rejected with a bare 400 Bad Request and no explanation.

5. 20 gwei is a protocol floor on Arc, not a suggestion

maxFeePerGas below 20 gwei can leave a transaction pending indefinitely rather than failing quickly. I set maxFeePerGas to twice the observed base fee and a 1 gwei tip.

6. Mainnet is open — verify that before you plan around credentials

The docs mention a permissioned phase, so I tested the write path before funding anything: sign a transaction, send it, read the error.

{"error":{"code":-32003,"message":"insufficient funds for gas * price + value"}}
Enter fullscreen mode Exit fullscreen mode

That is a normal EVM simulation failure, which means the node accepted and fully validated the transaction. No allowlist, no API key. Worth checking yourself rather than assuming.

The numbers that do not flatter it

  • The pool holds $2.55. Not a typo.
  • A $1 buy moves the price about 40%. A $5 buy moves it about 200%.
  • The $1,040 that aggregators print is arithmetic on those $2.55. It is not a valuation and nobody paid it.
  • No utility, no community, no exchange listing.

Deploying a token is easy and costs cents. That was never the hard part, and any write-up that stops at "look, it deployed" is hiding the interesting question.

What I deliberately did not do

No volume bots. No wash trading between wallets I control. No paid boosts. No "buy before it moons".

There is exactly one swap on the pool, it is 0.05 USDC, and it exists because DexScreener's documentation requires it to index the pair at all. It is labelled as an activation step in the repository, at the exact line where it happens.

It would have been easy to make the numbers look better. That is precisely why I did not.

Links

Happy to go deeper on any of the six points above — they cost me a few hours each, and I would rather they cost you nothing.

Top comments (0)