Oracle3
Open-source trading engine and MCP server for prediction markets. Fee-aware no-arbitrage trading across related contracts, live on Kalshi, Polymarket and Solana or on paper.
Set an exit condition. Exit Keepa watches your DeFi position and executes the permitted exit when the condition is met.
FinanceTypeScript · JavaScriptGitHub @levithefirstOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
1. Live app — https://exit-keepa-web.vercel.app
2. On-chain proof — BaseScan: 0xc8a00cc2…49fd8b
3. Demo video — https://youtu.be/WzgAuy__2Qo
Live projects: Aave v3, Gnosis Safe, Zodiac Roles. Exit Keepa is the
permissioned write path. KeeperHub is the execution layer.
"If the monitored rate crosses my threshold, withdraw my position back
to my Safe."Read the full README ↗0xc8a00cc2…49fd8bLive projects: Aave v3, Gnosis Safe, Zodiac Roles. Exit Keepa is the permissioned write path. KeeperHub is the execution layer.
"If the monitored rate crosses my threshold, withdraw my position back to my Safe."
Exit Keepa integrates KeeperHub as the deterministic execution layer for protective exits of Aave v3 positions held in user-owned Gnosis Safes, constrained by Zodiac Roles. Aave and Safe are the live systems users already use; Exit Keepa + KeeperHub are how a rate-based exit condition becomes a simulated-then-broadcast withdraw that lands onchain without ever giving the agent the user's keys.
Built for KeeperHub's Agent Economy hackathon
(dorahacks.io/hackathon/agent-economy).
See docs/SUBMISSION.md for the full submission.
npm install && npm run judge
Prints the live-proof transaction and execution id, the live URLs with a
real health probe, the agent surfaces, whether broadcasting is disabled,
and the current test counts — each derived from the code rather than
quoted, so none of it can quietly rot. Add --fast to skip running the
suites.
Three things a judge can click, with no wallet and no new transaction:
| Audit trail | /audit — one exit at a time, in order: condition snapshot → policy verdict → simulation → KeeperHub execution id → receipt. Seeded with the real on-chain execution, so there is real data to read before doing anything. Live-proof rows and demo-sandbox rows are labelled on every row and never mixed. |
| A blocked call | On /audit, "Try a blocked call" builds a withdraw that pays out to 0x…dEaD instead of your Safe and shows Exit Keepa refusing it. It contacts KeeperHub not at all, stores nothing, and can never broadcast. |
| The live proof | The BaseScan link above. Pre-existing chain history — a judge verifies it rather than triggering it. |
Full click-through: docs/JUDGE_DEMO.md. Click
"Try demo" in the nav bar to walk the whole flow against a private
sandbox Safe, with no wallet extension.
packages/mcp) — five tools
over the API's own execution helpers: evaluate_exit_condition,
build_exit_calldata, simulate_exit, get_execution_status,
get_live_proof. Read-and-simulate only: it has no broadcast tool,
and simulate_exit refuses outright if asked to. Run it with
node packages/mcp/bin/exit-keepa-mcp.mjs.docs/workflows/aave-usdc-protective-exit.json)
— the protective exit described as a KeeperHub workflow: rate condition
→ policy gate → simulated execTransactionWithRole → a broadcast step
that is written down but disabled and env-gated. Schema-validated in
tests; never registered or executed. The live proof went through
Direct Execution REST, not this surface.GET /api/live-proof serves the canonical record
with the policy check recomputed per request. No session required.EXIT_KEEPA_ALLOW_BROADCAST defaults to unset. No broadcast code path in
this repository runs unless it is exactly the string "1" — not "true",
not "yes", not "1 ". The MCP server is stronger than gated: it has no
broadcast implementation at all, and setting the flag does not add one.
Exit Keepa has exactly one path that has ever sent a transaction —
apps/api/src/execution/executeApproved.ts, the autonomous path that
produced the proof above — and this work deliberately did not add a
second.
Exit Keepa v1 supports exactly one protocol and one action, deliberately, rather than a generic "call any protocol" system:
withdraw(asset, amount, to) on Aave's Pool contract —
withdraws a USDC supply position back to your Safesupply_apr/borrow_apr/utilization
vs. a basis-point threshold) you define per strategyEvery address and function selector below was independently verified, not guessed:
| Aave v3 Pool (Base) | 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 — cross-checked against bgd-labs/aave-address-book's AaveV3Base.sol, the canonical address list Aave Labs itself maintains |
| USDC (Base) | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Function | withdraw(address asset, uint256 amount, address to) |
| Selector | 0x69328dec — computed locally via keccak256, not recalled |
This requires the Safe to already hold a USDC supply position on Aave v3
Base (i.e. you've separately called supply() there). No approve step
is needed for the exit itself — an aToken burn is internal to the Pool
contract.
Safe → Zodiac Roles Modifier → execTransactionWithRole → Safe executes
This path was independently verified earlier in this project: a real,
harmless self-call transaction was broadcast and confirmed on Base
(0xf2122ab591a73f81c1d1290f4830d5576866e0a5e817d9687450f2b11481278c).
KeeperHub is the executor, but it can only ever call
execTransactionWithRole on your Roles Modifier — and the Roles Modifier
only permits what's explicitly scoped to a role. A role permits nothing
by default. The fully-scoped target for Exit Keepa's withdraw action
is:
scopeTarget(roleKey, targetAddress) — targetAddress = the Aave Pool
(0xA238Dd80C259a72e81d7e4664a9801593F98d1c5). Moves the target's
clearance from None to Function, so no function is callable on it
until step 2 explicitly allows one.scopeFunction(roleKey, targetAddress, selector, conditions, options)
— selector = 0x69328dec (withdraw), options = None (plain
call, no value, no delegatecall), with a parameter condition fixing
asset == USDC and to == the Safe itself (amount left
unrestricted). Once this is live, the role can withdraw USDC from
Aave and the funds can only ever land back in the Safe that owns them.What's actually granted on the live-proof Safe right now is that fully-scoped end state — both steps are live onchain:
scopeTarget has been applied, so the Aave Pool's
clearance is Function, not whole-target allowTarget.withdraw selector (0x69328dec) is allowed
for the role with parameter conditions fixing asset == USDC and
to == the Safe itself (amount left unrestricted), submitted by the
Safe's own owner
(tx 0x41d61e34...e81f1)
and independently re-verified by decoding that transaction's own
calldata directly from Base RPC — not assumed from the transaction
merely succeeding. See docs/ROLES_TIGHTENING.md
for the full verification record. No other function on the Aave
Pool is callable through this role, and a withdraw call is only
permitted if asset is USDC and to is this exact Safe — enforced by
the Roles Modifier itself, before Exit Keepa's own application-level
policy check (agent/policy.ts's assetBound/recipientBound, see
below) ever runs. That application-level check still runs too — this
is defense in depth, not a replacement for it.Wildcard clearance on the Safe
address itself in the Roles config. That's residual from earlier demo
setup, unrelated to the Aave withdraw path above — it does not grant
anything against the Aave Pool, and should not be read as part of this
permission's scope. It's flagged here so a judge inspecting the Roles
config directly isn't misled by it.A real Aave v3 USDC withdraw has been executed end-to-end through this exact path — see "Live proof" below — proving the architecture works against real chain state, under the permission state described above.
agent/poller.ts, on by
default in production) evaluates every active strategy on an interval.
Each tick reads the live Aave supply/borrow rate on Base, checks it
against your condition, and — only on a genuine edge crossing, claimed
atomically so exactly one attempt happens per crossing no matter how
many pollers are running — runs a deterministic policy check (right
chain, right contract, right function, right asset, funds returning
only to your Safe, Roles configured). Condition not met: nothing
happens. Policy check fails: the attempt is refused with a specific
reason and nothing reaches KeeperHub.execTransactionWithRole with
simulate: true, against the real Roles Modifier and Aave Pool). A
simulation that says the transaction would revert is a hard stop:
failed, nothing broadcast. A clean simulation is broadcast
immediately and automatically — the identical call with
simulate: false and an Idempotency-Key (KeeperHub's Safe
First-Write Sequence), keyed to this execution's own stable id so a
retried request replays instead of double-broadcasting. Both this
autonomous path and the manual/admin recovery endpoint call one
canonical service (execution/executeApproved.ts); there is exactly
one broadcast lifecycle in the codebase, with exactly one definition
of every guard on it.executionId is
saved as soon as it's known, then GET /execute/{executionId}/status
is polled with backoff (honoring X-Poll-Interval-Hint) until a
receipt confirms the outcome. A transaction hash is only ever
shown/stored once a receipt says so — never fabricated, and never
trusted from the self-reported status alone. An outcome that can't be
confirmed is reported as , never
as success and never as failure. The strategy page shows what
happened, with a BaseScan link for any receipt-confirmed transaction;
a still-confirming execution keeps re-checking on its own.The links and the one-command summary are at the top of this file; this
section is the detail behind them. See
docs/JUDGE_DEMO.md for the exact click path
(under 5 minutes), and docs/SUBMISSION.md for the
full pitch and honest-limitations list. The demo video is at
https://youtu.be/WzgAuy__2Qo; the shot list and script behind it are in
docs/DEMO_VIDEO_SCRIPT.md.
docs/JUDGE_DEMO.md §2-4.)agent/poller.ts) runs every
AGENT_POLL_INTERVAL_MS (default 30s) for every active strategy — on
by default in production, off by default in development, test, and
preview, and overridable in either direction with AGENT_POLL_ENABLED.
Each tick calls the same evaluateStrategy the on-demand "Check
now" button calls, reads the live Aave rate from Base rather than a
value typed in, and on a real crossing goes all the way through policy
→ simulate → broadcast → status verification without anyone present.
Every decision, including the ones that do nothing, is recorded with a
full receipt: intent, observation, policy check, simulation result,
KeeperHub response, final on-chain result.This is pre-existing chain history, not something a judge triggers. A real Aave v3 USDC withdraw was broadcast end-to-end through this exact Safe → Roles Modifier → KeeperHub path on Base mainnet, before this submission — the judge path below points at it rather than asking anyone to re-broadcast against an already-emptied demo position:
| Tx hash | 0xc8a00cc28bf116acea722ab298d610bdbfc50a05b902aae5ab74d9da1849fd8b |
| BaseScan | https://basescan.org/tx/0xc8a00cc28bf116acea722ab298d610bdbfc50a05b902aae5ab74d9da1849fd8b |
| Result | Receipt status 0x1 (success) — USDC returned to the Safe |
| Safe | 0xfFd5c5e17e09E012C99550Bfb2ef88d370cd66a9 (Base) |
| Roles Modifier | 0x694C3F6104741901F6AE0191Fd1afA9A274dBbBE |
| KeeperHub executor | 0xc68f0E22Dc6eD7e883873B36f23DdBBC1b3968Ac |
This was independently verified against the chain itself (not just the
app's own claim) via eth_getTransactionReceipt. The app's own
execution-history row for this broadcast briefly showed status: failed
due to a hash-extraction bug (fixed — see the executor tests) even though
the on-chain transaction had already succeeded; the chain, not the
database, is the source of truth here.
GET /api/auth/nonce, POST /api/auth/verify, POST /api/auth/demo-session
(apps/api/src/routes/auth.ts) implement a SIWE-style flow: sign a
one-time nonce with personal_sign, exchange the recovered signature for
a bearer session token, and every subsequent request to a Safe/strategy/
execution/agent-decision route is checked against a safe_owners table
(apps/api/src/auth/session.ts), so a caller can only ever read or act
on Safes they registered themselves. See
docs/SUBMISSION.md for the full test list.
apps/web Next.js (App Router) — wallet connect, dashboard, strategy CRUD, simulate/execute UI, audit trail
apps/api Express + Drizzle ORM — strategy/execution state machine, KeeperHub integration
packages/shared Types, Zod schemas, the Aave v3 Base protocol module (address constants + calldata encoder),
the canonical live-proof record, and the workflow-artifact schema
packages/mcp MCP server — a read-and-simulate adapter over apps/api's own execution helpers
Key backend pieces:
packages/shared/src/protocols/aaveV3Base.ts — the only place that
encodes a withdraw call; throws rather than encoding anything for a
non-USDC asset.apps/api/src/execution/buildTransaction.ts — the only place that
turns a stored strategy into a transaction. Never accepts a
target/function/calldata from a caller.apps/api/src/execution/executor.ts — calls KeeperHub's
execTransactionWithRole with simulate: true/false; only trusts a
transaction hash that passes real hex-format validation.apps/api/src/execution/stateMachine.ts — the broadcast idempotency
rule (decideBroadcast), enforced with a conditional UPDATE ... WHERE status = 'simulated' so two concurrent requests can never both
broadcast, and a retry after a successful broadcast is always a no-op.npm install
cp .env.example apps/api/.env
cp .env.example apps/web/.env.local # only NEXT_PUBLIC_* vars are read
Fill in apps/api/.env with a real DATABASE_URL, KEEPERHUB_API_KEY,
and KEEPERHUB_WEBHOOK_SECRET. Fill in apps/web/.env.local with
NEXT_PUBLIC_API_URL=http://localhost:4000. Never commit either file.
Run migrations against your database:
npm run db:migrate
Start both apps in dev mode (separate terminals):
npm run dev:api # http://localhost:4000
npm run dev:web # http://localhost:3000
Health checks: GET /health on the API, GET /api/health on the web app.
npm test # all three suites
npm run judge # the same counts, plus everything else a judge needs
Or one at a time:
npm run test --workspace packages/shared
npm run test --workspace apps/api
npm run test --workspace packages/mcp
323 tests total at the time of writing — 254 in apps/api, 36 in
packages/shared, 33 in packages/mcp. Rather than trusting that number
here, run npm run judge: it reports the counts by actually running the
suites, which is the only version that cannot go stale.
What they cover: chain-boundary enforcement (rejecting a Safe registered
on any chain other than Base before building a Base-targeted
transaction), calldata correctness (against independently-computed hex
fixtures, not the encoder checking itself), condition-comparator logic,
execution state-transition/idempotency rules, KeeperHub response parsing
(including refusing to trust a malformed hash, distinguishing a confirmed
KeeperHub rejection from an ambiguous network/timeout failure, and the
Safe First-Write Sequence's Idempotency-Key/status-polling handling), and
end-to-end tests (apps/api/test/e2e.test.ts,
apps/api/test/auth.e2e.test.ts) that walk create strategy → activate →
condition check → simulate → broadcast → duplicate-broadcast rejection →
recorded transaction hash, against an in-memory fake of the database and
a mocked KeeperHub client.
The tests added for the agent surfaces are specifically about what must not happen:
simulate_exit never sends simulate: false — asserted by inspecting
every simulate value the KeeperHub client was handed, with only the
network boundary mocked so the request under inspection is the one the
production code builds (packages/mcp/src/tools.test.ts).EXIT_KEEPA_ALLOW_BROADCAST is exactly
"1", and refused again with a different, explicit reason when it is
(there is no broadcast implementation in the MCP surface to enable).recipientBound and nothing else, and
never contacts KeeperHub at all
(apps/api/src/execution/blockedCall.test.ts, apps/api/test/e2e.test.ts).get_live_proof returns the exact canonical hash and ids, makes no
network call, and cannot return a freshly-generated one.packages/shared/src/workflows/keeperhubWorkflow.test.ts).packages/mcp/src/server.test.ts).See .env.example for the full list; all are validated at process
startup (apps/api/src/env.ts, apps/web/lib/env.ts) — the app refuses
to boot with missing/malformed required configuration.
| Variable | Where | Notes |
|---|---|---|
DATABASE_URL | API | Neon Postgres connection string |
KEEPERHUB_API_KEY | API | Never logged or exposed to the client |
KEEPERHUB_WEBHOOK_SECRET | API | HMAC secret for /api/webhooks/keeperhub |
BASE_CHAIN_ID / BASE_RPC_URL | API | Default to Base mainnet. BASE_RPC_URL must be a key-authenticated provider endpoint (Alchemy/Infura/QuickNode) in any deployed environment - verifying one Safe issues a batch of reads (module list, module bytecode + Roles invariants, role storage slots, five negative probes), and the free public mainnet.base.org rate-limits (HTTP 429) before that completes. A read that cannot complete fails closed to "could not verify", never to "protected". Set it as a host environment variable; never commit the key. |
CORS_ORIGINS | API | Set to * in production (this API has no cookie/session auth to protect) - unset means CORS is fully disabled and every browser request from the deployed frontend fails |
NEXT_PUBLIC_API_URL | Web | Must point at the deployed API's public URL |
The Nav's "Profile" button (apps/web/components/ProfileLoginModal.tsx)
offers a traditional username/password sign-up/login as an alternate login
method alongside "Connect wallet" - a persistent account for someone who
wants to build and review strategies without a wallet extension. No
external provider and no extra configuration: passwords are hashed with
scrypt and a per-account random salt (apps/api/src/auth/password.ts),
stored in the local_accounts table (see Database below).
Schema: apps/api/src/db/schema.ts (Drizzle ORM). Tables:
safe_accounts — chain, Safe address, optional Roles Modifier address + role keyexit_strategies — condition (jsonb) + action (jsonb, validated ExitAction) + statuskeeperhub_executions — one row per execution attempt: idempotency key (unique), status, request/response payloads, tx hash, broadcast/confirmed timestamps, error messagelocal_accounts — username/password profiles: unique username, scrypt password hash + saltaudit_events — append-only log of every state changeMigrations live in apps/api/drizzle/ with a proper meta/ journal
(earlier migrations in this project's history were never actually
runnable — no journal existed — which is why the previous production
database had zero tables; this is fixed).
npm run db:generate --workspace apps/api # after changing schema.ts
npm run db:migrate --workspace apps/api # apply migrations
KeeperHub surfaces used. Every write this project has ever made went
through the plain REST Direct Execution API. packages/mcp adds an
agent-facing adapter over the same helpers, but it is read-and-simulate
only — it has no broadcast tool, so it is not a second write path:
POST /execute/contract-call — execTransactionWithRole against the
Roles Modifier, simulate: true first, then simulate: false once
clean (apps/api/src/keeperhub/client.ts's callContractFunction).Idempotency-Key header on every broadcast, sourced from the
execution row's own stable id — never minted fresh per HTTP attempt.
idempotentReplay, 409 idempotency_conflict, and
409 idempotency_in_progress are each handled as their own case, not
folded into a generic error.GET /execute/{executionId}/status polling with backoff, honoring
X-Poll-Interval-Hint. receipts[] (independently re-fetched from the
chain by KeeperHub) are treated as authoritative over the self-reported
status/transactionHash fields.GET /chains — confirms Base is enabled.POST /execute/check-and-execute — would
stay Roles-bound (the action leg is still execTransactionWithRole),
but its condition check requires a single-scalar contract read, and
Exit Keepa's real trigger (Aave's supply APR) only exists inside a
15-field struct return with no single-scalar getter. See
docs/SUBMISSION.md §4 for the full reasoning.apps/api/src/keeperhub/client.ts and apps/api/src/keeperhub/types.ts
document, inline, exactly what was live-verified against KeeperHub's real
API this session (contract-call resolution, the functionArgs JSON-
stringified-array quirk, the execTransactionWithRole simulation and
real-broadcast response shapes) versus what's implemented directly from
KeeperHub's own published Direct Execution API reference but not yet
independently live-exercised by this project (the Idempotency-Key and
status-polling paths — see docs/keeperhub-integration.md's
"Documented (2026-09-01)" section and docs/SUBMISSION.md's "what still
breaks" list for the honest gap). apps/api/src/execution/executor.ts is
written to never trust an unverified shape — see its doc comments.
See docs/keeperhub-integration.md and docs/zodiac-verification-evidence.md
for the full research trail.
Backend (Railway) — live:
exit-keepa / service api, tracking branch
claude/exit-keepa-init-v5lzuyhttps://api-production-2e11.up.railway.appnpm install && npm run build:api (Railway dashboard setting —
note this overrides apps/api/railway.json's build/deploy fields
for this service; the dashboard config is authoritative)npm run db:migrate --workspace apps/api —
idempotent, safe to run on every restartnpm run start --workspace apps/api/healthFrontend (Vercel) — live:
exit-keepa-web, tracking branch claude/exit-keepa-init-v5lzuyhttps://exit-keepa-web.vercel.appapps/web; Install Command runs the real monorepo
npm install, Build Command runs npm run build:web — see
docs/VERCEL_DEPLOY.md for the exact
settings and the two build-breaking misconfigurations that were fixed
to get here (Root Directory, and the real npm install needing to run
as the Install Command rather than buried inside the Build Command).Database (Neon): already provisioned; migrations run automatically on every Railway deploy via the pre-deploy command above.