Case study + build guide

How we built an Abstract upvote bot

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.

Daily cardTelegram reminder at vote window
Session keyvote-only permissions
Preflight firstno blind transactions
Mika's Abstract penguin mascot
01 · Problem

Manual upvotes are easy to miss.

Abstract Portal streaks reward consistency. The annoying part is remembering the daily window, especially when you are busy, sleeping, or traveling.

🗳

Daily signal

Upvotes are a repeatable activity signal inside the Abstract ecosystem. The bot protects the habit without removing human control.

Window logic

The script reads Portal vote data, checks whether today is already done, and waits until the safe daily window opens.

Tap to confirm

The bot does not silently fire. Telegram sends a card, and the vote broadcasts only after explicit confirmation.

02 · Architecture

The flow we built.

A small self-hosted system: OpenClaw Telegram account, Abstract Portal API checks, AGW session key, cron scheduler, and narrow transaction policy.

Bot watches. Human approves. Session key votes.

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.

Telegram botCronAGW sessionPortal APIPreflight checks
03 · Proof pack

Not just a pattern — a running artifact.

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.

Onchain proof

We created a limited AGW session for the upvote flow and tested a real Portal vote path on Abstract.

  • AGW session creation tx: 0xa5c0…4031
  • Agent AGW test vote: 0x0c61…de4
  • Main AGW session vote for voteForApp(207) succeeded and was confirmed by Portal API.

Current streak metrics

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

Telegram card shipped

The production reminder sends a native Telegram card with two explicit actions.

✅ Отправить upvote → abs_main_upvote:send:<appId>
❌ Пропустить       → abs_main_upvote:no:<appId>

Latest candidate: Onchain Heroes (appId 25)
Deadline: 04.05.2026, 18:00 MSK

Abstract-specific notes

  • Portal vote contract: 0x3B50dE27506f0a8C1f4122A1e6F470009a76ce2A.
  • Vote selector: 0x7060a227 / voteForApp(uint256).
  • Portal APIs used: /api/user/<address>/votes and /api/user/<address>/vote-streak.
  • AGW client lesson learned: session execution must use a compatible @abstract-foundation/agw-client version; an older client produced an invalid magic value failure until upgraded.
04 · Step-by-step guide

Build the bot from zero.

A practical sequence someone can follow: setup, Telegram, session key, scripts, cron, testing, and recovery.

0

Understand the target flow

The bot should not vote blindly. It should watch the Abstract Portal window, notify you in Telegram, then broadcast only after you tap confirm.

InputWallet address, Telegram chat, Portal vote status, allowed app IDs.
OutputA confirmed voteForApp(appId) transaction through a scoped session key.
1

Prepare the server workspace

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
  • Use a dedicated folder for the bot.
  • Commit code, but never commit private keys or bot tokens.
  • Create an out/ folder for logs and daily run output.
2

Create a dedicated Telegram bot

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
  • Use a separate bot for upvotes so alerts are not mixed with normal chat.
  • Allow only your Telegram user/chat ID.
  • Send a test message before adding any wallet logic.
3

Read Abstract Portal vote status

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 important behavior: do not spam. The card appears only when there is something useful to do.
4

Create a limited AGW session key

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 })
  • Target contract: Abstract Portal vote contract.
  • Allowed selector: voteForApp(uint256).
  • ETH value: 0.
  • Fee cap: small, explicit limit.
  • Expiration: set a real end date.
Never use your seed phrase or main private key. The bot should only hold the limited session signer secret.
5

Write the preflight script

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
  • Check votedToday=false.
  • Check selected app is allowlisted and not already voted this epoch.
  • Check session has not expired.
  • Check target contract, selector, value, and fee cap match policy.
  • Estimate gas / simulate before broadcast.
6

Write the Telegram confirmation flow

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)
}
This is the key trust model: the server reminds you, the human approves the transaction, and the backend refuses duplicate confirmations.
7

Write the broadcast script

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"
  • Reject if confirmation string is missing or wrong.
  • Reject if preflight fails.
  • Re-run preflight after the button tap, not only when the card was sent.
  • Do not blindly retry after a timeout; first check logs, Portal state, and tx status.
  • Return concise Telegram success/failure text.
  • Include tx hash on success.
8

Schedule the daily reminder

Use cron to run the daily card sender after the Portal window opens. Log every run so you can inspect failures.

CRON_TZ=Europe/Moscow
5 15 * * * cd ~/abstract-upvote-bot && \
  node tools/abstract-upvotes/send-daily-card.mjs \
  >> tools/abstract-upvotes/out/daily-card.log 2>&1
Cron uses the server timezone unless you set CRON_TZ. VPS machines often run UTC, so define the timezone explicitly or convert the Portal window yourself. In our build, we used the Portal nextVoteBy value to derive the safe window and added a small grace period.
9

Test in this order

Do not jump straight to production cron. Test each layer separately so any failure is easy to isolate.

  • Telegram test message arrives.
  • Portal status checker returns the wallet vote state.
  • Daily card sends only when votedToday=false.
  • Preflight passes for the chosen app.
  • Manual confirm broadcasts one successful vote.
  • After success, Portal shows votedToday=true.
  • Next daily cron sends the card at the expected time.
05 · Implementation map

Files behind the bot.

The exact filenames from our build, shown as a reproducible map rather than leaking secrets.

Daily Telegram card

Checks Portal state and sends the confirm/skip card.

tools/abstract-upvotes/send-daily-card.mjs

Preflight

Validates wallet state, app id, session policy and gas before anything is sent.

tools/abstract-wallet/prepare-session-vote-tx.mjs

Broadcast

Runs only after confirmation and submits the vote through the session key.

tools/abstract-wallet/broadcast-session-vote-tx.mjs

Production cron shape

The real job runs once per day and writes logs. Time should be adjusted to the Portal vote window for the wallet.

CRON_TZ=Europe/Moscow
5 15 * * * cd /path/to/workspace && \
  node tools/abstract-upvotes/send-daily-card.mjs \
  >> tools/abstract-upvotes/out/daily-card.log 2>&1
06 · Safety

The important part.

This is not “give a bot your wallet.” The whole point is narrow permissions, simulation/preflight, and human confirmation.

🔒

No seed phrase

The bot never needs the main wallet seed phrase or main private key. If a guide asks for that, stop.

🎯

Vote-only scope

The session policy is limited to the Portal vote contract, the vote selector, zero ETH value, and a small fee cap.

🧪

Preflight first

Before broadcast: check not already voted, app is allowed, session not expired, policy matches, gas is covered, simulation passes.

🔁

Session rotation

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.

🧷

Idempotency lock

Every confirm button should have a lock like wallet + appId + epoch, so double taps and network retries cannot submit duplicate votes.


Rule: the Telegram button is confirmation only for this one narrow upvote policy. Do not reuse this pattern for token approvals, transfers, swaps, or broad wallet permissions without stronger review.
07 · Deploy + operate

Keep it running safely.

Production is mostly about boring reliability: logs, narrow permissions, and no duplicate retries.

A

Run with cron or a process manager

The daily card is perfect for cron. If you add a long-running watcher later, use PM2 or systemd.

B

Watch the first 2–3 runs

Check logs after the first scheduled windows. Confirm the card timing, button behavior, and Portal status after a successful vote.

C

Avoid duplicate retries

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.

D

Rotate the session key before expiry

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.


Troubleshooting checklist

  • No Telegram card: verify bot token, chat ID, allowlist, and network access.
  • Card arrives but button fails: check callback route and confirm string.
  • Preflight fails: check app allowlist, votedToday, session expiry, and policy selector.
  • Session execution fails: verify AGW client version matches the session creation flow.
  • Vote succeeds but streak not updated: re-check Portal API after a short delay and verify the app ID.
Result

A daily Abstract upvote assistant that protects streaks without taking wallet control.