BeaconHarbor
HomeQueueExploreHarborHow it risesMechanicsLightkeeper
Arc mainnet
Developers

Read the harbor from the chain

Every number BeaconHarbor shows comes from Arc. Read the queue, the launches and every trade straight from the contracts, or from the cached REST API served next to this site. No key and no account. There is no package to install and no SDK: the samples below carry the ABI fragment they need, so viem is the only dependency.

Network

ChainArcChain ID5042Public RPChttps://rpc.mainnet.arc.iohttps://rpc.blockdaemon.mainnet.arc.iohttps://rpc.quicknode.mainnet.arc.iohttps://rpc.drpc.mainnet.arc.ioExplorerhttps://explorer.arc.ioGas tokenUSDC, nativeFinalityOne confirmation. A confirmed block on Arc is final.

These are the public endpoints this deployment ships with. They are shared and their operators rate limit them, so anything that polls should use its own endpoint.

Contracts

Launchpad0x3eAeB58b84bd6DF915c1C74585BeE7855d9470FFLaunch queue0x2bCCD924CdCdC9D65Cd2c9886402eadF03244a46USDC0x3600000000000000000000000000000000000000Token and poolOne pair per launch, created by the launchpadGraduator0xed6068c922A95DeB3576b00868bb7E83a317FcF4Tax hook on Uniswap v40xC88c224F9c499d020250b26e08298989afB360CcSwap router0x939C561fA2c9D6400d4687165D5aBb45b628f464

Every pool address comes from the launchpad. poolId(pool) returns 0 for any address the launchpad did not create, so check a pool there before you trust it. The official $BEACON pool is the one beaconPool() returns on the queue, and nothing else.

A graduated token's Uniswap v4 pool id is v4PoolId() on its curve pool and poolIdOf(token) on the graduator. The hook keeps the tax terms and the claimable USDC per pool id, and the router is how the site swaps on that pool with plain approvals. Any other Uniswap v4 route reaches the same pool and pays the same tax.

How a launch reaches the chain

1. Register. The creator calls register on the queue with the launch request, the X account id the creator proved and a signed registration permit. The queue holds 1,000 $BEACON and any dev-buy USDC, then emits Registered and RegisteredRequest with the whole request inside it.

2. Wait for the turn. Only the head of the line can be resolved, and at most 5 launches land in a round of 60 seconds of block time.

3. Execute. Anyone calls execute with the head id and the exact request. The queue calls launchFor on the launchpad, which creates the token and its pool, and the queue emits LaunchExecuted. Genesis also emits GenesisLaunched and fixes beaconToken and beaconPool for good.

4. Graduate. Inside the buy that raises the graduation threshold, LaunchPool._graduate burns the surplus and calls HarborGraduator.graduate, which registers the tax with HarborHook, initializes the Uniswap v4 pool at the last curve price and mints one full-range position it owns for good. The pool emits Graduated with the v4 pool id. From then on swaps run on Uniswap through any route, the hook taking the creator's tax in USDC, and the max wallet and the block budget are over.

Before genesis, register reverts. The founder wallet written into the contract calls registerGenesis instead, the ticker BEACON is refused for everyone else, and standard registration opens the moment the genesis launch succeeds.

LaunchQueue

The order, the rounds, what a registration holds, and the only path to a new launch.

Reads

head() returns (uint256)The id of the registration at the head of the line. Nothing behind it can launch first.nextQueuedId() returns (uint256)The first id that is still queued, skipping cancelled ones. This is what execute takes. 0 means nothing to run.queueLength() returns (uint256)How many ids are still unresolved, cancelled ones included.slotsLeft() returns (uint256)Launches this round can still take.currentWindow() returns (uint256)The round number, which is the block timestamp divided by the round length.windowEndsAt() returns (uint256)Unix seconds when the current round ends.launchedInWindow(uint256 window) returns (uint8)Launches that landed in a given round. This is the number a beacon is lit from.getRegistration(uint256 id) returns (Registration)One registration, with its status, what it holds and the token and pool once it launched.getRegistrations(uint256 fromId, uint256 count) returns (Registration[])A page of registrations, oldest first.isEligible(address) returns (bool)Whether an address holds enough $BEACON to register right now. False for everyone before genesis.minBeaconBalance() returns (uint256)The $BEACON balance a standard registration needs, in token units.beaconToken(), beaconPool()The genesis token and its pool, both zero until genesis launches. These identify the official $BEACON.genesisDone(), genesisId()Whether genesis has launched. genesisId names its registration.founder() returns (address)The only address allowed to register genesis. Rotating it is timelocked.attestor() returns (address)The address whose signature the queue accepts on a registration permit. Rotating it is timelocked.launchGas() returns (uint256)The gas the queue hands to a launch. The owner can only raise it, up to a cap in the contract.totalEscrow(), totalBeaconLocked()USDC held for dev buys, and $BEACON held with registrations, across the whole queue.hashRequest(LaunchParams req) returns (bytes32)The requestHash of a request, so you can check a stored one without encoding it yourself.checkRequest(LaunchParams req, bool genesis)Reverts with the same reason register would, so a client can show the refusal before asking for a signature.WINDOW(), MAX_LAUNCHES_PER_WINDOW(), MIN_BEACON_WHOLE()The round length in seconds, the launches a round allows, and the whole $BEACON a registration holds.openByRegistrant(address), openByX(uint64), ticketUsed(bytes32)The open-registration guards: one per wallet, one per X account, and each permit nonce single use.

Writes

register(LaunchParams req, uint64 xUserId, uint8 role, bytes32 nonce, uint64 expiry, bytes sig)Register a standard launch. Approve the $BEACON and any dev-buy USDC to the queue first. Returns the id.registerGenesis(LaunchParams req, uint64 xUserId, uint8 role, bytes32 nonce, uint64 expiry, bytes sig)The same call for genesis, from the founder wallet, with the ticker BEACON and no $BEACON held.execute(uint256 id, LaunchParams req)Resolve the head. Pass the id from nextQueuedId and the exact request from the RegisteredRequest event. Permissionless, and it grants the sender no place and no priority.cancel(uint256 id)Cancel your own queued registration and give up its place. What it holds becomes claimable.claimRefund(uint256 id)Take back the dev-buy USDC of a registration that did not launch.claimBeacon(uint256 id)Take back the $BEACON a resolved registration held.

Events

Registered(id, registrant, genesis, xUserId, role, requestHash, escrow, nonce)A new registration, with the registrant, the X account id, the role and the requestHash.RegisteredRequest(id, req)The whole launch request in the log, which is how an executor or an indexer rebuilds it without the API.LaunchExecuted(id, token, pool, window)A launch landed, with its token, its pool and the round it counted against.GenesisLaunched(id, token, pool), GenesisAdopted(token, pool)The genesis launch, emitted once. GenesisAdopted covers a genesis pool adopted at deploy time.Cancelled(id), Skipped(id)A registration the registrant cancelled, and Skipped when execute clears a cancelled head.LaunchFailed(id, reason)The launch reverted inside the launchpad. The raw revert bytes are in the log, and what the registration held stays claimable.Ineligible(id, reason)A registration resolved without launching because it no longer met the rules. Reason 1 is the $BEACON balance.BeaconLocked(id, amount), BeaconClaimed(id, amount), RefundClaimed(id, amount)$BEACON held and returned, and dev-buy USDC returned.

A registration, field by field

getRegistration returns one struct. Every view on this site is built from it.

address registrantThe wallet that registered, and the creator of the launch.bool genesisTrue only for the genesis registration.uint8 statusWhere the registration stands. See the values below.uint8 role0 Project, 1 Developer, as chosen for the X account in the launch form.uint64 xUserIdThe X account id the creator proved. It is public on X.uint64 registeredAt, resolvedAt, registeredBlockUnix seconds when it was registered and when it resolved, plus the block it was registered in.bytes32 requestHashkeccak256 of the encoded request. execute refuses anything else.uint256 escrowDev-buy USDC held with the registration, in 6-decimal units. 0 once it is spent or returned.uint256 beaconLocked$BEACON held with the registration, in 18-decimal units. 0 once it is claimed back.address token, address poolThe token and its pool, set when the launch succeeds.

Status values

  • 0 None. No registration with this id.
  • 1 Queued. Waiting for its turn.
  • 2 Launched. The token and the pool exist.
  • 3 Cancelled. The registrant gave up the place.
  • 4 Ineligible. Kept in the ABI. With the $BEACON held by the queue, a balance cannot drop before execution, so no current path reaches it.
  • 5 Failed. The launch reverted, and what it held is claimable.

Read the queue

Any EVM library works. This reads the round, the head of the line and the registration that is up next, in one batch through Multicall3.

import { createPublicClient, http, parseAbi } from "viem"; const queueAbi = parseAbi([ "struct Registration { address registrant; bool genesis; uint8 status; uint8 role; uint64 xUserId; uint64 registeredAt; uint64 resolvedAt; uint64 registeredBlock; bytes32 requestHash; uint256 escrow; uint256 beaconLocked; address token; address pool; }", "function slotsLeft() view returns (uint256)", "function windowEndsAt() view returns (uint256)", "function nextQueuedId() view returns (uint256)", "function getRegistration(uint256 id) view returns (Registration)", "function getRegistrations(uint256 fromId, uint256 count) view returns (Registration[])", ]); const client = createPublicClient({ transport: http("https://rpc.mainnet.arc.io") }); const queue = { address: "0x2bCCD924CdCdC9D65Cd2c9886402eadF03244a46", abi: queueAbi } as const; // the round, the head of the line and what is up next const [slots, endsAt, next] = await Promise.all([ client.readContract({ ...queue, functionName: "slotsLeft" }), client.readContract({ ...queue, functionName: "windowEndsAt" }), client.readContract({ ...queue, functionName: "nextQueuedId" }), ]); // the next entries in the line, oldest first const upcoming = await client.readContract({ ...queue, functionName: "getRegistrations", args: [next, 10n], });

Execute the head

Execution is open to anyone, which is why a keeper outage cannot block the line. Take the id from nextQueuedId, rebuild the request from that id's RegisteredRequest log, and send execute. The contract checks the order, the round and the request hash, so a wrong request or a wrong turn reverts. One exception: a call that first cleared cancelled heads keeps that work and returns without launching, so read the LaunchExecuted event rather than the receipt status.

import { parseAbi, parseAbiItem } from "viem"; const params = "(string name, string symbol, string metadataURI, uint256 supply, uint8 tokenType," + " uint16 buyTaxBps, uint16 sellTaxBps, address feeRecipient, uint256 devBuyUsdc, uint256 minDevTokens)"; const id = await client.readContract({ ...queue, functionName: "nextQueuedId" }); const reg = await client.readContract({ ...queue, functionName: "getRegistration", args: [id] }); const [log] = await client.getLogs({ address: "0x2bCCD924CdCdC9D65Cd2c9886402eadF03244a46", event: parseAbiItem(`event RegisteredRequest(uint256 indexed id, ${params} req)`), args: { id }, fromBlock: reg.registeredBlock, toBlock: reg.registeredBlock, }); await wallet.writeContract({ address: "0x2bCCD924CdCdC9D65Cd2c9886402eadF03244a46", abi: parseAbi([`function execute(uint256 id, ${params} req)`]), functionName: "execute", args: [id, log.args.req], gas: 6500000n, });

The queue refuses to start a launch it cannot hand the full gas stipend, so send a generous gas limit. That stipend starts at 5,000,000 and the owner can only raise it, up to 15,000,000. The platform keeper sends 6,500,000.

What it refuses, and why

emptyNothing is queued.round fullThis round is full. The next one opens at the end of the current minute of block time.requestThe request does not hash to the one the registration stored.gasThe transaction did not carry enough gas for the launch stipend, so nothing changed.Skipped(id)A cancelled head is skipped rather than launched, which costs no slot. If the round is full after that, or the new head is not the id you passed, the call returns successfully with the skips kept and nothing launched.

The request and its hash

A launch request is one struct, in this order. The queue stores keccak256 of its ABI encoding, so the request that launches is byte for byte the one that was registered.

struct LaunchParams { string name; // at most 48 bytes string symbol; // ^[A-Z0-9]{1,12}$ string metadataURI; // at most 256 bytes uint256 supply; // 1,000, 100,000, 1,000,000, 100,000,000, 1,000,000,000 tokens, in 18-decimal units uint8 tokenType; // 0 Normal, 1 Tax uint16 buyTaxBps; uint16 sellTaxBps; address feeRecipient; uint256 devBuyUsdc; uint256 minDevTokens; } requestHash = keccak256(abi.encode(params))

supply is one of the five allowed supplies in 18-decimal units, tokenType is 0 Normal or 1 Tax, the taxes are basis points between 100 and 1000 on each side or 0 on both for a Normal token, feeRecipient may be the zero address to pay the registrant, and devBuyUsdc is 6-decimal USDC.

The same hash is available on-chain as hashRequest(req), and checkRequest(req, genesis) reverts with the reason register would give.

Metadata URI

The chain stores only a URI, at most 256 bytes of it. BeaconHarbor writes a document served by this site and addresses it by the sha256 of its own bytes, so the address proves the content.

GET https://beaconharbor.app/api/harbor/meta/<sha256>.json {"category":"Meme","description":"...","image":"/api/harbor/uploads/logo-<sha256>.png", "links":{"discord":"","telegram":"","tiktok":"","website":""},"logoSha":"<sha256>","logoStatic":false, "name":"Token name","role":"project","schema":1,"symbol":"TICKER", "x":{"id":"<x account id>","username":"handle","verifiedType":"none"}}
  • The document is canonical JSON with sorted keys and no whitespace. The sha in the path is the sha256 of exactly those bytes, which you can recompute from the response body.
  • A metadata document is served with a one-year immutable cache. It is never rewritten, because a change would change its address.
  • The x block is a snapshot taken when the document was created: the X account id, the username at that moment and the verified type X reported. Usernames change on X, so treat the id as the identity and the username as a label.
  • A launch from another launchpad can carry any URI at all. Nothing forces a token to use this scheme.

REST API live · same origin

A cached API served next to the site under /api/harbor, fed by the event indexer, so history never leans on a rate-limited RPC. Every number it returns comes from an event the chain emitted or from a snapshot of a chain read. The queue snapshot is refreshed every 3 seconds, so no request of yours costs an RPC call.

  • GET /api/harbor/launch/readiness · what this deployment actually has: chain, contracts, genesis, X verification, attestor, keeper, indexer
  • GET /api/harbor/queue/state · the live round: slots, recent launches, who is waiting, and the estimate for each
  • GET /api/harbor/queue/registration/{id} · one registration, with its estimate while it waits
  • GET /api/harbor/queue/by/{address} · every registration of an address, with what it can claim back
  • GET /api/harbor/queue/executing · the keeper's in-flight launch transaction, if it has one
  • GET /api/harbor/index/status · indexer cursor, trades and holders counted so far
  • GET /api/harbor/token/{pool}/stats · holders, 24h USDC volume and trade count for a pool
  • GET /api/harbor/token/{pool}/trades · points of light, the buys and sells of a pool, with transaction hashes
  • GET /api/harbor/token/{pool}/holders · top holders of a pool and their share of supply
  • GET /api/harbor/meta/{sha}.json · the metadata document of a launch, by its sha
  • GET /api/harbor/feed?tab=newest · posts, ranks and boosters in the harbor
  • GET /api/harbor/leaderboards?window=1h · most reposted and most volume, by window

Queue and readiness answers are sent with no-store: read them again rather than caching them.

An error body is a JSON object with code, an optional field, optional params and an English error sentence for older clients. Match on code, never on the sentence.

Rate limits

Token buckets per client IP, before any body is parsed. Burst is what a fresh client may spend at once, then the bucket refills at the rate shown.

burst 240, then 600 a minutePublic reads, the queue and token endpoints includedburst 20, then 60 a minuteWallet reads that can miss the cache and reach an RPCburst 30, then 30 a minuteWrites that need a sessionburst 10, then 10 a minuteWallet sign-inburst 20, then 12 a minuteSaving a draft or a launch postburst 10, then 10 a minuteLogo upload and metadata creationburst 10, then 10 a minuteX account verificationburst 10, then 10 a minuteRegistration permits

Over the limit the answer is 429 with the code rate_limited, a Retry-After header and the seconds to wait in params.

Fair data This API only ever returns what the chain emitted or what a wallet published here. No fabricated volume, no invented holders, no filler price.


BeaconHarbor

Launches take turns. Built against pump and dump.

Language

Platform

Launch queueExploreRegister a beaconThe Harbor

Trust

MechanicsFirst LightLightkeeper

Resources

FAQAPITermsArc docs (opens in a new tab)

Community

X (opens in a new tab)Telegram (opens in a new tab)TikTok (opens in a new tab)
© 2026 BeaconHarbor · beaconharbor.appInterface previews use sample data. Not financial advice. The contracts have not been audited.