/** * Enter an Orbio agent's trading wallet in Orblivion's prize league, then show the entry and the standings. * * npm run enter -- AGENT_ID the trading wallet is the agent's owner: it signs * npm run enter -- AGENT_ID --owner-key PATH the agent's owner is another wallet: its key file signs * npm run status this wallet's entries and the standings (nothing is signed) * * --wallet-key PATH the trading wallet's key file (default ~/.orblivion-bot/wallet.json, the starter bot's wallet) * --api-url URL Orblivion's API (default: ORBLIVION_API_URL if set, else https://api.orblivion.com) * * The trading wallet is the one whose trades count: it signs in. The agent's owner, as Orbio's vault records it, signs * the entry statement, which carries the owner's eligibility confirmation. Before anything is signed, the statement is * shown, checked against the text the SDK builds itself, and signed only after you type SIGN. Signing sends no * transaction and costs no gas. Key files ({"private_key": "0x..."}, the starter bot's format) stay outside this folder * and are never printed. * * @orblivion/sdk 0.1.1 doesn't pass on the owner that Orblivion's statement names (a later release does). With it, the * script stops before signing when the signing key isn't the owner's; without it, Orblivion checks the signature against * the vault's owner and refuses the entry (NOT_OWNER). */ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { createInterface } from 'node:readline/promises' import { fileURLToPath } from 'node:url' import { ELIGIBILITY_CONFIRMATION, OrblivionError, createOrblivionClient, leagueStatementText, type LeagueStatement, type OrblivionClient, } from '@orblivion/sdk' import type { Hex } from 'viem' import { privateKeyToAccount, type PrivateKeyAccount } from 'viem/accounts' const HERE = path.dirname(fileURLToPath(import.meta.url)) const WALLET_KEY = '~/.orblivion-bot/wallet.json' const LEAGUE_PAGE = 'https://orblivion.com/league.html' const TOP = 10 /** What to do after some of Orblivion's refusals. */ const HINTS: Record = { NOT_OWNER: "the owner Orbio's vault records signs: give its key file with --owner-key, or use the League page", AGENT_ENTERED: 'this agent already has a prize entry: the wallet that entered it, or the owner, can leave first', ALREADY_ENTERED: 'this wallet already plays for another agent: leave first (client.league.leave())', NOT_AN_AGENT: "check the agent's number (#...) on the Orbio launchpad: it isn't its ticker", STATEMENT_EXPIRED: 'a statement works once, for 10 minutes: run the script again', SANCTIONED: 'no entry is possible for an address on the sanctions list', } /** A problem the person running the script has to fix: shown as a message, not a stack trace. */ class Stop extends Error {} /** Other people's words (names, symbols, reasons) as plain text: no control characters reach the terminal. */ const clean = (s: unknown, n = 40) => String(s ?? '').replace(/[\p{Cc}\p{Cf}]/gu, '?').slice(0, n) const utc = (ts: number) => { const [day, dd, mon, year, hms] = new Date(ts * 1000).toUTCString().replace(',', '').split(' ') // Mon 02 Nov 2026 00:00:00 GMT return `${day} ${Number(dd)} ${mon} ${year} ${hms!.slice(0, 5)} UTC` } // --- keys (outside this folder, never printed) --------------------------------------------------------------------------- function loadKey(file: string, what: string): PrivateKeyAccount { const full = path.resolve(file.startsWith('~') ? path.join(os.homedir(), file.slice(1)) : file) const rel = path.relative(fs.realpathSync(HERE), fs.existsSync(full) ? fs.realpathSync(full) : full) if (!rel.startsWith('..') && !path.isAbsolute(rel)) { throw new Stop(`Keep the ${what}'s key file outside this folder, so it is never shared or committed with the code.`) } if (!fs.existsSync(full)) throw new Stop(`No ${what} key file at ${full}.`) try { return privateKeyToAccount(JSON.parse(fs.readFileSync(full, 'utf8')).private_key as Hex) } catch { throw new Stop(`${full} isn't a key file: it holds {"private_key": "0x..."}.`) } } // --- the statement ------------------------------------------------------------------------------------------------------- /** The SDK already refused any statement unlike its own text (LEAGUE_STATEMENT). Checked here again, in the open: the same * text, rebuilt from the agent, this wallet, the nonce and the expiry, carrying the confirmation word for word, and not * expired. */ function checkStatement(st: LeagueStatement, agentId: number, wallet: string) { const want = leagueStatementText(agentId, `prize league: trading wallet ${wallet}`, wallet, st.nonce, st.expiresAt) if (st.message !== want || !st.message.split('\n').includes(ELIGIBILITY_CONFIRMATION)) { throw new Stop("The statement isn't the text the SDK builds for this agent and wallet: nothing was signed.") } if (st.expiresAt <= Date.now() / 1000) throw new Stop('The statement has expired: run the script again.') } /** The owner's personal_sign of the checked statement: the trading wallet's own (the SDK signs only its own prize * statement), or the owner's key file's. */ const sign = (client: OrblivionClient, owner: PrivateKeyAccount | null, message: string): Promise => owner ? owner.signMessage({ message }) : client.signLeagueStatement(message) async function enter(client: OrblivionClient, agentId: number, owner: PrivateKeyAccount | null) { const wallet = client.address.toLowerCase() const signer = (owner ? owner.address : wallet).toLowerCase() const lg = client.league const next = (await lg.summary()).nextSeason const mine = (await lg.me()).entries.filter((e) => e.board === 'prize' && e.status === 'active') if (mine.length && mine[0]!.agentId !== agentId) { throw new Stop(`This wallet already plays the prize league for agent #${mine[0]!.agentId}: one entry per wallet. ` + 'Leave first (client.league.leave()), then enter again.') } if (mine.length && mine[0]!.confirmed) { console.log(`Already entered for agent #${agentId}, and the owner's confirmation is on file.\n`) return status(client) } const confirming = mine.length > 0 // entered without the owner's confirmation (by signing in): confirm it now const st = await lg.statement({ agentId }) // signs in as the trading wallet; refused unless it is the SDK's own text checkStatement(st, agentId, wallet) // 0.1.1 drops the owner Orblivion names; a later SDK passes it on const named = (st as LeagueStatement & { owner?: string | null }).owner const vaultOwner = named ? named.toLowerCase() : null console.log(`Agent #${agentId}`) console.log(`Trading ${wallet} (its trades count)`) console.log(`Owner ${vaultOwner ?? 'not named by this SDK version: Orblivion checks the signature'} (the Orbio vault's record: it signs)`) if (!confirming) console.log(`Plays from Season ${next.number}: ${utc(next.startsAt)} to ${utc(next.endsAt)}`) if (vaultOwner && vaultOwner !== signer) { throw new Stop(`\nThe Orbio vault records ${vaultOwner} as agent #${agentId}'s owner, not ${signer}: nothing was signed.\n` + (owner ? "Use the owner's key file, " : "Pass the owner's key file with --owner-key, ") + `or enter on the League page (${LEAGUE_PAGE}), where the owner signs in a browser wallet.`) } const expires = new Date(st.expiresAt * 1000).toISOString().slice(11, 19) console.log(`\nThe statement the owner signs (identical to the SDK's own text; it expires ${expires} UTC):\n`) for (const line of st.message.split('\n')) console.log(` | ${line}`) console.log('\nSigning confirms the line that starts "I am 18 or older" in the owner\'s name: 18 or older, not a US person, ' + 'not in a sanctioned country or region or on a sanctions list, eligible under the laws that apply, and the Prize ' + 'League Rules accepted. Under the rules the owner confirms it for itself and for whoever controls the trading ' + 'wallet. Sign only if it is true.') const rl = createInterface({ input: process.stdin, output: process.stdout }) const answer = await rl.question("Type SIGN to sign it as the agent's owner: ") rl.close() if (answer.trim() !== 'SIGN') throw new Stop('Not signed. Nothing was entered.') const signature = await sign(client, owner, st.message) if (confirming) { const e = await lg.confirm({ agentId, signature, nonce: st.nonce }) console.log(`\nConfirmed entry ${e.entryId}: the owner's eligibility confirmation is on file.`) } else { const e = await lg.enter({ agentId, signature, nonce: st.nonce }) console.log(`\nEntered entry ${e.entryId}: agent #${e.agentId}, trading wallet ${e.wallet}, from Season ${e.fromSeason}`) console.log(`Confirmed ${e.confirmed ? 'yes' : 'no'} (proof: ${e.proof})`) } console.log() return status(client) } // --- the entry and the standings ----------------------------------------------------------------------------------------- async function status(client: OrblivionClient) { const lg = client.league const me = client.address.toLowerCase() const { entries } = await lg.me() console.log(`Your entries (${me})`) for (const e of entries) { const conf = e.confirmed === null ? '' : e.confirmed ? ', confirmed' : ', NOT confirmed: no prize' console.log(` ${e.board.padEnd(8)} agent #${String(e.agentId).padEnd(6)} entry ${String(e.entryId).padEnd(4)} ` + `${e.status.padEnd(6)} from Season ${e.fromSeason}${conf}`) } if (!entries.length) console.log(' none') const sd = await lg.standings() console.log(`\nPrize league, Season ${sd.season.number} (${sd.status}): ${utc(sd.season.startsAt)} to ${utc(sd.season.endsAt)}`) if (!sd.rows.length) { console.log(sd.status === 'upcoming' ? ' No standings yet: entrants appear once the season starts.' : ' No entrants.') return } console.log(` ${'rank'.padStart(4)} ${'agent'.padEnd(8)} ${'symbol'.padEnd(10)} ${'profit USD'.padStart(12)} ${'trades'.padStart(6)} eligibility`) const usd = (s: string) => Number(s).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) for (const r of [...sd.rows.slice(0, TOP), ...sd.rows.slice(TOP).filter((x) => x.wallet === me)]) { console.log(` ${String(r.rank ?? '-').padStart(4)} ${('#' + r.agentId).padEnd(8)} ${clean(r.symbol, 10).padEnd(10)} ` + `${usd(r.profitUsd).padStart(12)} ${String(r.trades).padStart(6)} ${r.eligibility?.status ?? '-'}${r.wallet === me ? ' <- you' : ''}`) } for (const r of sd.rows.filter((x) => x.wallet === me)) { console.log(`\nYour entry's eligibility, rule by rule (agent #${r.agentId}):`) for (const rule of r.eligibility?.rules ?? []) console.log(` ${rule.rule.padEnd(13)} ${rule.status.padEnd(8)} ${clean(rule.reason, 110)}`) } } // --- main ---------------------------------------------------------------------------------------------------------------- function args(argv: string[]) { const out: { agentId?: number; ownerKey?: string; walletKey: string; apiUrl?: string; status: boolean } = { walletKey: WALLET_KEY, status: false } for (let i = 0; i < argv.length; i++) { const a = argv[i]! const value = () => { const v = argv[++i] if (v === undefined) throw new Stop(`${a} needs a value`) return v } if (a === '--owner-key') out.ownerKey = value() else if (a === '--wallet-key') out.walletKey = value() else if (a === '--api-url') out.apiUrl = value() else if (a === '--status') out.status = true else if (/^[0-9]{1,9}$/.test(a) && Number(a) >= 1 && out.agentId === undefined) out.agentId = Number(a) else throw new Stop(`Unknown argument ${clean(a, 40)}. Usage: npm run enter -- AGENT_ID [--owner-key PATH] [--wallet-key PATH] [--api-url URL], or npm run status`) } if (out.agentId === undefined && !out.status) throw new Stop("Give the Orbio agent's number (npm run enter -- 282), or --status.") return out } try { const a = args(process.argv.slice(2)) const wallet = loadKey(a.walletKey, 'trading wallet') let owner = a.ownerKey ? loadKey(a.ownerKey, 'owner') : null if (owner && owner.address.toLowerCase() === wallet.address.toLowerCase()) owner = null // the same wallet signs for itself const client = createOrblivionClient({ baseUrl: a.apiUrl, account: wallet }) console.log(`Orblivion ${client.http.baseUrl}\n`) await (a.status ? status(client) : enter(client, a.agentId!, owner)) } catch (e) { if (e instanceof Stop) console.error(e.message) else if (e instanceof OrblivionError) { const hint = HINTS[e.code] console.error(`\nRefused: ${e.code}: ${e.message}${hint ? `\n (${hint})` : ''}`) } else throw e process.exitCode = 1 }