Composable execution infrastructure for Web3 intents. Intents are resolved by an on-chain ExecutionEngine that scores competing IExecutionModule implementations (via ScorePolicy) and executes the best one, rather than routing through a single monolithic solver.
The repo is a monorepo. Every package/app under packages/ and apps/ named in the README's repository-structure diagram is now implemented: packages/contracts, packages/types, packages/sdk, packages/config, apps/execution-node, apps/indexer, apps/api, apps/frontend, and apps/landing (see below). The top-level scripts/ directory holds one local-dev convenience script (deploy-local.sh, see below); docs/ is still not populated — see the scan-scope rules below for docs/.
-
packages/contracts(Foundry/Solidity) — the on-chain execution core.src/core/—ExecutionEngine.sol(entrypoint:executeIntent, module selection loop) andIntentRegistry.sol(owner-gated intent-type registration).src/modules/—ExecutionModuleBase.sol(abstract base implementingIExecutionModule),RouterModule.solandMevProtectionModule.sol(two competing modules registered for the sameROUTEintent type, proving out real module-selection behavior beyond test mocks).src/policy/—ScorePolicy.sol, the pluggable weighted-scoring contractExecutionEnginecalls to rank module simulation results. Scores are signed (int256) — a module whose penalties outweigh its quality legitimately scores negative rather than reverting. Weights are governable post-deploy via the owner-gatedupdateWeights(), effective immediately.src/registry/—ModuleRegistry.sol, owner-gated intent-type → active-module-address mapping.src/types/—ExecutionQuote.sol, the canonical struct modules return fromsimulate()andScorePolicyscores.src/interfaces/—IExecutionModule.sol,IExecutionQuote.sol.src/access/—ProtocolRoles.sol, the single shared owner (owner/transferOwnership/isOwner) thatModuleRegistry,IntentRegistry, andScorePolicyall defer to instead of each holding their ownownerstate. Deliberately not multi-role RBAC for now.- There is no
src/settlement/— a standalone settlement layer was dropped from the design (see README). The winning module'sexecute()call is the on-chain settlement; there's no separate settlement step. test/— Foundry tests (ExecutionEngine.t.sol,ModuleCompetition.t.sol— real competing modules, not mocks, exercisingScorePolicyweighting — andGovernance.t.sol—ProtocolRoles/ScorePolicygovernance) plustest/mocks/(mock modules used inExecutionEngine.t.sol).script/Deploy.s.sol— deploys the 5 core kernel contracts (ProtocolRoles→ registries/ScorePolicy→ExecutionEngine) for a givenPROTOCOL_OWNER, with zero intents/modules registered by default — the shape a real customer deployment needs (seedocs/architecture/provisioning.md). The demoROUTEintent + both example modules are wired only whenDEPLOY_DEMO_MODULES=trueis explicitly set, and only whenPROTOCOL_OWNERis left to default to the broadcaster (demo wiring registers on-chain as the broadcaster, so it can't also honor a separate owner in the same run).run()parses env vars and delegates todeploy(DeployParams), which holds the actual logic and is whattest/Deploy.t.solcalls directly.
-
packages/types— zero-runtime-dependency TypeScript mirrors of the on-chain types:ExecutionQuote,ScorePolicy.Weights, module/intent registration shapes and events.uint256fields arebigint; theScorePolicy.evaluate()score is signed (ExecutionScore). Consumed as TS source (no build step) — seesdk/execution-nodebelow. -
packages/sdk— aviem-based typed client (intent/,execution/,registry/,abi/subdirs).createExecutionKernelClient(...)bundles one client per contract (intentRegistry,moduleRegistry,scorePolicy,execution,module);intentBuilder.intentType(label)hashes a label exactly like Solidity'skeccak256("label"). EveryonlyOwner/mutating call simulates viapublicClient.simulateContractbefore submitting.examples/quickstart.tsis a runnable, verified end-to-end example against a localanvildeployment — read it before writing new sdk code. -
apps/execution-node— the off-chain pipeline consumingpackages/sdk:engine/intentProcessor.ts(raw request →Intent),engine/executionGraphBuilder.ts(gas-free off-chain preview of whatExecutionEnginewould currently select — reproduces the on-chain scoring exactly via read calls, doesn't reimplement it),solvers/solver.ts(one generic solver, not per-module — every module is scored identically, so arouterSolver/mevSolversplit would be redundant boilerplate today),execution/executor.ts(submission).index.ts'srunIntent(...)ties process → solve → submit together. "Execution graph" here means the current one-round competing-modules model, not chained multi-module execution (still just a future direction — see README). -
apps/indexer— off-chain observability, also consumingpackages/sdk.listeners/eventListener.tsis generic over(address, abi, eventName)(backfill viagetContractEvents+ a livewatchContractEventvariant) rather than one listener per contract — every contract's events decode the same way through viem.processors/kernelEventProcessor.tsbackfills all 5 kernel contracts' events intodb/memoryStore.ts(deliberately in-memory — no real DB dependency until persistence across restarts actually matters).metrics/executionMetrics.tsderivestotalExecutions/executionsByModule/moduleWinRatefrom the store.index.ts'screateIndexer(...)backfills into a fresh store and returns it with the metrics bound. -
packages/config—localAnvil(aviemChain),localAnvilAddresses/localAnvilModules(the deterministic addresses that fall out of deployingProtocolRoles → IntentRegistry → ModuleRegistry → ScorePolicy → ExecutionEngine → RouterModule → MevProtectionModule, in that order, from anvil's default account #0 on a fresh chain — not a persistent deployment),ROUTE_INTENT_TYPE, andLOCAL_API_URL(apps/api's local dev default). Deliberately does not depend onpackages/sdk(defines its own structurally-equivalent address shape instead) sincesdk's ownexamples/quickstart.tsdepends onconfig— aconfig → sdk → configcycle would exist otherwise. No real testnet/mainnet chain entry exists yet; add one only when an actual deployment happens. -
apps/api— Fastify, consumingsdk/execution-node/indexer/config. Deliberately read-only: registry state (intentsController,modulesController), off-chain gas-free predictions (/modules/:intentType/predict, wraps execution-node'ssolve()), indexer metrics (metricsController:/metrics/executions,/metrics/executions/:intentType/win-rate/:module), and raw per-transaction history (executionsController:GET /executions?intentType=&limit=, most-recent-first, capped at 100 — exposesapps/indexer's individualIntentExecutionRecords, which previously only the aggregate metrics endpoints surfaced). Every controller backfills a fresh indexer from block 0 on every request — fine on local anvil's handful of blocks, revisit before pointing this at a chain with real history. No execute/submit route: that would mean the API custodying a private key on callers' behalf, with its own auth/rate-limiting design to work out first — a separate decision, not folded in here.utils/json.ts'stoJsonSafe()is required on every response — Fastify's default serializer throws onbigint(most reads return one) and silently drops aMap(executionsByModule()'s return type) to{}. Defaults to port4000(not3000— that collides withapps/frontend's defaultnext dev/next startport, and the two are meant to run side by side), with open CORS registered (@fastify/cors) sinceapps/frontendfetches it cross-origin. -
apps/frontend— Next.js (App Router — notpages/, despite the README's original sketch predating the App Router becoming the default;src/app/holdslayout.tsx/page.tsx/providers.tsx), React, TypeScript,wagmi+viem. Styled as a restrained "protocol console" (Tailwind v4 token system inglobals.css:--bg/--surface/--border/--ink/--muted/--accent+ a--success/--warning/--dangertrio kept visually distinct from--accent— every component consumes these tokens, both themes fully specified, not just<body>; Geist Sans/Mono, already fetched by the original scaffold but previously overridden by a stray literalfont-family: Arial, now actually used: Sans for UI text, Mono for every technical value). Unlikeapps/api, this one does submit transactions — through the user's own connected wallet (useWalletClient()), never a server-held key, so the custody concern that keptapps/apiread-only doesn't apply here.components/execution/ExecutionConsole.tsxis the primary surface: intent → every candidate module's real score, not just the winner's (CandidateModuleRow.tsx, visually distinct "Selected" state) → execute (explicit phase text: submitting/confirming/confirmed/failed, plus block number and gas used, not just a bare tx hash) → protocol metrics.components/protocol/OverviewStats.tsxandcomponents/metrics/RecentExecutions.tsx(real per-transaction history via a newapps/api/executionsendpoint — deliberately doesn't show historical candidate scores, since the indexer never persisted the losing candidates' quotes at execution time; fabricating them would violate the app's own data-integrity rule) sit in a side rail on desktop, stacking on mobile.components/layout/AppHeader.tsxalways shows network identity (right vs. wrong chain, connected or not) — a wallet-writing app with no such indicator was a real gap; a wrong-network wallet would otherwise fail silently at execute time.services/kernelClient.tsbridges wagmi's viem clients into sdk'screateExecutionKernelClient(...);hooks/wrap that in React Query (useIntents,useModules,usePrediction— reuses execution-node'ssolve()directly, since it's pure viem and works fine in the browser, not just Node;useExecutionMetrics/useRecentExecutionsare the two hooks that do NOT talk to the chain directly — they callapps/api, completing the chain-event → indexer → api → frontend loop instead of the frontend re-deriving this itself).state/isn't populated — local component state is enough at this app's current complexity; don't add a global store speculatively.lib/wagmiConfig.tssetsssr: true— required, not optional: without it wagmi's hooks report "disconnected, no connectors" on the server but read the real wallet on the client's first render, and React throws that mismatch away as a hydration error (any real visitor with a wallet extension installed hits this, not just a test). Only connects tolocalAnvil(frompackages/config) — needs a browser wallet pointed at a local anvil node to use.e2e/full-flow.spec.ts(Playwright) — the actual "connect wallet → read registry → construct intent → simulate → wallet confirmation → executeIntent() → transaction confirmed → indexer observes event → API exposes updated metrics → frontend displays result" flow, driven through a real headless browser against the real dev server, real wagmi hooks, real sdk calls, a realanvilchain, and the realapps/indexer/apps/apipackages. No real wallet extension exists to automate, so the one thing replaced is the wallet's confirmation popup:page.addInitScriptinjects an EIP-1193 provider (both legacywindow.ethereumand an EIP-6963 announcement, so wagmi'sinjected()connector finds it either way) that proxies every RPC call straight toanvil, which auto-signseth_sendTransactionfor its own unlocked default accounts — no private key material touches the browser. Geteth_accountsvseth_requestAccountsright if you touch this: the former must return[]until the latter has actually been called once, or wagmi silently auto-reconnects on page load before the test (or a real user) ever clicks anything. Requiresanvil(fully deployed, same sequence aspackages/sdk/examples/quickstart.ts) andapps/apialready running beforenpm run e2e;playwright.config.tsmanages only the frontend dev server itself.
-
apps/landing— the public marketing site forexekpro.com, deliberately separate fromapps/frontend:frontendis the technical protocol console (wallet-connected, chain-reading);landingis a static, no-wallet product page explaining the architecture to prospective developers/customers. Has zero@execution-kernel-protocol/*dependencies and nowagmi/viem/React Query — pure marketing content, so it carries none of the console's runtime complexity. One deliberate dark theme (not the console's adaptive light/dark), reusing the same Geist Sans/Mono fonts for brand continuity without a shared design-token file (the two apps' visual identities are intentionally distinct — console is a data-dense tool, landing is a product page).lib/links.tscentralizes the only three external URLs the page uses (GitHub repo, a doc anchor, and the console athttps://exekpro.com/—apps/frontend's own Cloudflare Worker, deployed alongside this app athttps://exekpro.com/about). Content is careful never to claim mainnet deployment, real customers, revenue, or an open permissionless module ecosystem — none of which exist yet.
Dependency direction: packages/types → packages/contracts (mirrors on-chain types) → packages/sdk (wraps contract bindings) → apps/execution-node/apps/indexer (both consume the SDK: one to orchestrate execution, the other to observe it) → apps/api/apps/frontend (api consumes sdk+execution-node+indexer together, read-only; frontend consumes sdk+execution-node directly from the browser, and can write via the user's own wallet). packages/config sits alongside sdk (depends only on types, not sdk) and is consumed by sdk/execution-node/indexer's example scripts plus apps/api/apps/frontend directly. apps/landing sits outside this dependency graph entirely — it consumes nothing from packages/. Never point a dependency the other way (e.g. contracts must not import from sdk, and config must not import from sdk).
- Priority for security review, in order:
packages/contracts/src/core,packages/contracts/src/modules,packages/contracts/src/policy,packages/contracts/src/access. These hold the intent-execution and module-selection trust boundary. - Do not read
packages/contracts/test/mocksordocs/unless the task explicitly asks for them — mocks are test fixtures, not production logic, anddocs/is currently just a placeholder. packages/contracts/out/andpackages/contracts/cache/are Foundry build artifacts, gitignored — never read or grep them; if they're missing, that's expected, not an error.node_modules/anywhere in the repo is gitignored and never worth reading/grepping — same rule asout//cache/.- Before any analysis of the contracts, run
forge build(frompackages/contracts/) first to surface compile errors before reasoning about the code.
- After editing anything under
packages/contracts/src/core/orpackages/contracts/src/modules/, always runforge test(frompackages/contracts/) before considering the task done. - After editing anything under
packages/types/,packages/sdk/,packages/config/,apps/execution-node/,apps/indexer/, orapps/api/, runnpm run typecheck(from the repo root) before considering the task done. None of these six have a build step — they're consumed as source, so a clean typecheck is the bar, not a successful build. - After editing anything under
apps/frontend/, runnpm run typecheck(repo root) same as the others, but also runnext build(fromapps/frontend/) — unlike the other six, this one is a real Next.js app with an actual build/bundle step, andtsc --noEmitalone won't catch everythingnext builddoes (e.g. the client/server component boundary). - Never commit changes touching
packages/contracts/src/core/orpackages/contracts/src/access/without first showing and getting explicit review of the diff — these are the trust-boundary paths.
Conventional Commits, scoped to this repo's packages:
<type>(<scope>): <description>
- Types:
feat,fix,refactor,test,docs,chore,perf - Scopes:
contracts,sdk,execution-node,types - One logical change per commit — don't bundle unrelated edits across scopes.
- Never use
--no-verify. - Squash WIP commits before merging.
- Ask before committing if the diff touches
access/, even if tests pass.