Daily signal
Upvotes are a repeatable activity signal inside the Abstract ecosystem. The bot protects the habit without removing human control.
A practical guide to the Telegram-confirmed daily upvote flow we built: Portal checks, session-key voting, cron reminders, safe preflight, and one-tap confirmation — wrapped in a playful Abstract design.

Abstract Portal streaks reward consistency. The annoying part is remembering the daily window, especially when you are busy, sleeping, or traveling.
Upvotes are a repeatable activity signal inside the Abstract ecosystem. The bot protects the habit without removing human control.
The script reads Portal vote data, checks whether today is already done, and waits until the safe daily window opens.
The bot does not silently fire. Telegram sends a card, and the vote broadcasts only after explicit confirmation.
A small self-hosted system: OpenClaw Telegram account, Abstract Portal API checks, AGW session key, cron scheduler, and narrow transaction policy.
The daily sender checks Portal state. If the wallet has not voted, it sends a Telegram card. When you tap confirm, a separate broadcast script uses a limited AGW session key to call the Portal vote contract.
This guide is a public builder case study for the Claw Council: the bot was wired, tested, scheduled, and used with real Abstract Portal state.
We created a limited AGW session for the upvote flow and tested a real Portal vote path on Abstract.
0xa5c0…40310x0c61…de4voteForApp(207) succeeded and was confirmed by Portal API.The daily card reads live Portal state before it asks for confirmation.
currentStreakDays: 4
longestStreakDays: 7
lastVoteAt: 2026-05-02T15:07:48Z
votedToday: false
nextVoteBy: 2026-05-04T15:00:00Z
The production reminder sends a native Telegram card with two explicit actions.
✅ Send upvote → abs_main_upvote:send:<appId>
❌ Skip → abs_main_upvote:no:<appId>
Latest candidate: Onchain Heroes (appId 25)
Reminder sent: 2026-05-03 15:05 UTC
Portal deadline: 2026-05-04 15:00 UTC
0x3B50dE27506f0a8C1f4122A1e6F470009a76ce2A.0x7060a227 / voteForApp(uint256)./api/user/<address>/votes and /api/user/<address>/vote-streak.@abstract-foundation/agw-client version; an older client produced an invalid magic value failure until upgraded.A practical sequence someone can follow: setup, Telegram, session key, scripts, cron, testing, and recovery.
The bot should not vote blindly. It should watch the Abstract Portal window, notify you in Telegram, then broadcast only after you tap confirm.
voteForApp(appId) transaction through a scoped session key.Use a small VPS or always-on machine. Install Node.js 20+, create a project folder, and keep secrets outside git.
mkdir -p ~/abstract-upvote-bot
cd ~/abstract-upvote-bot
npm init -y
npm install viem @abstract-foundation/agw-client
out/ folder for logs and daily run output.In Telegram, talk to @BotFather, create a new bot, copy the token, then send your bot one message so it can identify your chat.
# Example env shape — token values stay private
TELEGRAM_BOT_TOKEN=123456:bot_token_here
TELEGRAM_CHAT_ID=your_numeric_chat_id
The daily sender should ask Portal whether the wallet already voted today. If votedToday=true, it exits quietly. If not, it sends a Telegram card.
// pseudo-flow
const status = await getPortalVoteStatus(wallet)
if (status.votedToday) return
await sendTelegramCard({ appId, currentStreak, nextVoteBy })
The session key is a separate signer. It must be registered with the Abstract Global Wallet and scoped to exactly the upvote action.
// pseudo-code: create a vote-only session
const session = await agwClient.createSession({
signer: sessionSigner.address,
expiresAt: now() + days(14),
permissions: [{
target: ABSTRACT_PORTAL_VOTE_CONTRACT,
selector: selector("voteForApp(uint256)"),
valueLimit: 0n,
feeLimit: parseEther("0.0005")
}]
})
saveSecret("SESSION_SIGNER_PRIVATE_KEY", sessionSigner.privateKey)
savePublicConfig({ sessionId: session.id, expiresAt: session.expiresAt })
voteForApp(uint256).0.Before a vote can broadcast, preflight checks that the action is still valid and safe. This prevents duplicate votes, wrong app IDs, expired sessions, or policy mismatches.
node tools/abstract-wallet/prepare-session-vote-tx.mjs --app 207
votedToday=false.The daily card has two choices: confirm or skip. Confirm immediately acknowledges the Telegram callback, creates an idempotency lock, then calls the broadcast script with an exact confirmation string. Skip does nothing.
✅ Confirm upvote → broadcast-session-vote-tx.mjs
⏭ Skip today → no transaction
// prevent double-taps and timeout retries
const key = `${wallet}:${appId}:${portalEpoch}`
await answerCallbackQuery(callback.id, "Preparing upvote…")
if (await lock.exists(key)) return "Already processing"
await lock.create(key, { ttlSeconds: 120 })
try {
await runBroadcast({ appId, confirm: "CONFIRM_ABSTRACT_UPVOTE" })
} finally {
await lock.release(key)
}
Broadcast should be boring and narrow. It receives the confirmed app ID, re-runs preflight, then uses the session signer to submit the vote.
node tools/abstract-wallet/broadcast-session-vote-tx.mjs \
--app 207 \
--confirm "CONFIRM_ABSTRACT_UPVOTE"
Use cron to run the daily card sender after the Portal window opens. Log every run so you can inspect failures.
CRON_TZ=UTC
5 15 * * * cd ~/abstract-upvote-bot && \
node tools/abstract-upvotes/send-daily-card.mjs \
>> tools/abstract-upvotes/out/daily-card.log 2>&1
CRON_TZ. For a public, international workflow, UTC is the cleanest default. In our build, nextVoteBy is the Portal deadline, while the reminder runs after the safe daily window opens with a small grace period.Do not jump straight to production cron. Test each layer separately so any failure is easy to isolate.
votedToday=false.votedToday=true.The exact filenames from our build, shown as a reproducible map rather than leaking secrets.
Checks Portal state and sends the confirm/skip card.
tools/abstract-upvotes/send-daily-card.mjsValidates wallet state, app id, session policy and gas before anything is sent.
tools/abstract-wallet/prepare-session-vote-tx.mjsRuns only after confirmation and submits the vote through the session key.
tools/abstract-wallet/broadcast-session-vote-tx.mjsThe real job runs once per day and writes logs. Time should be adjusted to the Portal vote window for the wallet.
CRON_TZ=UTC
5 15 * * * cd /path/to/workspace && \
node tools/abstract-upvotes/send-daily-card.mjs \
>> tools/abstract-upvotes/out/daily-card.log 2>&1
This is not “give a bot your wallet.” The whole point is narrow permissions, simulation/preflight, and human confirmation.
The bot never needs the main wallet seed phrase or main private key. If a guide asks for that, stop.
The session policy is limited to the Portal vote contract, the vote selector, zero ETH value, and a small fee cap.
Before broadcast: check not already voted, app is allowed, session not expired, policy matches, gas is covered, simulation passes.
Treat session keys as temporary. Monitor expiresAt, alert before expiry, revoke old keys, and create a fresh vote-only session before the next daily window.
Every confirm button should have a lock like wallet + appId + epoch, so double taps and network retries cannot submit duplicate votes.
Production is mostly about boring reliability: logs, narrow permissions, and no duplicate retries.
The daily card is perfect for cron. If you add a long-running watcher later, use PM2 or systemd.
Check logs after the first scheduled windows. Confirm the card timing, button behavior, and Portal status after a successful vote.
If a local command times out but the gateway/server may still be processing, check logs/status before retrying. Telegram messages and votes are not things to spam twice.
Add a small reminder job or startup check that warns when the session has less than 48 hours left. Create a new vote-only session, update secrets, run preflight, then revoke or retire the old one.
A daily Abstract upvote assistant that protects streaks without taking wallet control.