
ContextLock
An agentic IDE to design, simulate, deploy and operate DeFi agents with on-chain spending limits.
Demo
About
ContextLock Studio is an agentic IDE for building confidential financial agents that can be trusted with money. Detailed Video at : https://youtu.be/MM4qlYC4SGU?si=O3XhylvHud5XgqLt THE PROBLEM Every agent that can move funds today is given a key and told to behave. The limits live in a system prompt, and a prompt is a suggestion, not a boundary. It takes one of: - an injected instruction from untrusted content - a stale oracle round that still parses and still looks reasonable - a confused tool call and the transaction is already signed. The model isn't malicious; it's obedient, and obedience is the vulnerability. Anything that can be talked out of enforcing a rule was never enforcing it. Guardrails that live inside the model's own context window are reachable by anything that can write into that context. WHAT WE BUILT A workspace where the thing being authored is not source code but an agent and its authority model. You describe an agent in natural language and the IDE compiles that into a versioned Blueprint: identity, data requirements with trust classes and freshness windows, permitted actions, and an explicit authority envelope. Every other surface is a projection of that artifact. The architecture graph, the generated code, the simulation runs and the deployed policy all carry a revision, and anything built against an older one is marked stale rather than shown as current. Authority itself is enforced by a deterministic policy layer sitting outside the model and outside the runtime. Every proposed action is evaluated against stated limits (amount, recipient allowlist, data trust class and age, rolling window budget) and resolves to one of three outcomes: - ALLOW, under the per-action limit, where the agent acts alone - ESCALATE, between that and the escalation band, where a human approves - DENY, above the hard ceiling, refused outright whatever the model believes When an action is permitted the agent receives a capability that is scoped to that single action, single-use, nonce-bound and already expiring. It never holds a standing key, so a compromised runtime inherits no authority beyond the capability currently in its hand. The one number we refuse to generate is the financial ceiling. The built-in assistant interviews you through every gap in your requirements: protocol, triggers, recipients, data sources, forbidden actions. But when it reaches the limits that bound your money it asks and then refuses to answer. A figure it suggested would become the figure you accepted. HOW YOU USE IT Describe the agent, generate a Blueprint, and inspect the permission matrix it produces, which is computed from the policy rather than described by the model. From there: - Run 24 deterministic simulation scenarios and 16 attack scenarios (prompt injection, recipient mutation, amount mutation, target mutation, stale oracle data, runtime compromise, mainnet write attempts) and see the exact layer that stopped each one, and whether a capability was ever issued - Test against real market conditions with mainnet as a read-only source or an isolated local fork, where fork transactions are labelled as such and never given a public explorer link - Deploy to a testnet with the policy starting disabled, and enable financial authority only once every precondition passes After deployment you operate it from the same workspace: observed on-chain state with per-source freshness, a correlated event log that traces any transaction back through capability issuance to its original trigger, drift detection between expected and observed configuration, and an Emergency Lock that attempts the financial policy first. INTEGRATIONS - Chainlink Data Feeds supply verified oracle rounds carrying a trust class and a freshness window, so a decision that depends on a source past its TTL is refused with an explicit reason code rather than taken on old numbers - Chainlink CRE runs the agent workflow, and the product tracks the provenance of every run, so a simulated result can never be presented as production execution - ENS gives every agent its own name, and with it its own policy hash, budget and keys: separate principals rather than one super-agent, each revocable independently, with revocation invalidating previously issued capabilities without touching what the policy permits - Ledger provides hardware signing over WebHID, with real device detection that walks connect, PIN, review, signed as the hardware changes state, the address derived on the device, and a rejection surfaced as a legitimate outcome rather than swallowed as an error
How it's made
THE SHAPE OF IT Next.js 15 App Router on React 19 and TypeScript. React Flow draws the architecture graph, Monaco backs the code surface, TanStack Query holds server state with a deliberately short stale time because observed chain state should never be served from cache as if it were current. The design system is hand-written CSS, no Tailwind and no component library, scoped under one root with container queries so a card responds to the pane it sits in rather than the viewport. Every page reads from a typed layer that mirrors the exact shapes the chain and the policy engine return: Blueprint revisions, policy matrices with reason codes, simulation runs with deterministic inputs and stopping layers, runtime events with correlation IDs. The domain types live in one file that every surface imports, so changing an artifact shape fails the typecheck everywhere it is wrong. CHAINLINK DATA FEEDS: WHERE FRESHNESS BECAME A TYPE A policy layer can enforce an amount on its own. What it cannot invent is whether the price it is deciding on is still true. Data Feeds return a round, not a number, and the round carries provenance: aggregator address, decimals, heartbeat, deviation threshold, and the timestamp of the last update. That shape is why freshness is a first-class field everywhere in this product rather than a nice-to-have. - Every data requirement in a Blueprint declares a trust class and a maximum age, for example VERIFIED_ORACLE with a TTL of 60 seconds - Before an action is evaluated, the round's age is checked against that TTL. Past it, the action is refused with ORACLE_STALE and no capability is issued at all - Trust class is a property of the source and cannot be promoted by prose. An indexed query cannot satisfy a verified-price requirement, and the built-in assistant is explicitly forbidden from claiming otherwise - Where a required source is unavailable it is reported UNAVAILABLE rather than silently substituted with a weaker one The attack suite exercises this directly: a stale price round and a source-unavailable scenario both stop at the policy layer, and the run records which layer stopped them. CHAINLINK CRE: A RUNTIME WITH A PROVENANCE TRAIL CRE gave us something to compile the agent's workflow to, and a boundary we would not have designed on our own. The workflow compiles to WASM and its hash is recorded in the deployment receipt alongside the runtime image digest, so the artifact that ran can be checked against the artifact that was approved. Runs execute through the official CRE CLI simulator, and every run carries its mode with it. That last part shaped the product. Because a simulated run and a production run are distinguishable in the data, we could make them distinguishable in the UI and refuse to let one be displayed as the other. The CRE surface states execution mode, DON status, consensus and hardware attestation as separate evidence flags rather than one green "connected" badge, and those flags travel into the exported safety report. A report cannot claim more than the run it was built from. ENS: IDENTITY AS A STRUCTURAL CONSTRAINT ENS changed the architecture more than any other integration. Agents live under an organisation namespace, each holding its own subname, node hash, owner and manager. Because identity is per-agent rather than per-deployment, "separate principals, not one super-agent" stopped being a convention and became something the product can check: - Each agent has its own ENS name, policy hash, budget and keys - Two agents resolving to the same policy hash is surfaced as a blocking critical issue, because one agent's authority would then be indistinguishable from another's - Resolver records carry agent context, an endpoint and resolver metadata - Revocation invalidates capabilities already issued under that identity, and leaves untouched what the policy permits That last line is the separation we think is correct and that ENS forced us to be explicit about: ENS identifies and revokes, the policy governs money. The UI refuses to let either stand in for the other, and no spending limit is ever stored as a text record, because a name record is not an authority boundary and nothing enforces it at execution time. LEDGER: THE ONE PART RUNNING ON REAL HARDWARE Signing goes over WebHID using @ledgerhq/hw-transport-webhid and @ledgerhq/hw-app-eth. viem serialises the unsigned EIP-1559 transaction, the device derives the address on the standard path, and the signature comes back as r, s and v from the hardware. Nothing about the flow is staged. The hacky part is that there is no API which reports "the Ledger is locked". The entire connection state machine is derived from a single getAddress() call and the status code it throws: - 0x5515: powered but locked, waiting on a PIN - any other status: answering, but the Ethereum app is not frontmost - an address returned: unlocked, correct app, ready to sign - 0x6985 during signing: the user pressed reject The signing promise stays pending for exactly as long as the user is reviewing on the device, which is what makes the "Review on your Ledger" panel literally true rather than a timed guess. A rejection is treated as an answer and surfaced plainly, not swallowed as an error, because for this product a refused signature is a legitimate outcome. Two details that cost real time. A 1.2 second poll runs alongside WebHID connect and disconnect events, because unplugging the cable fires an event but entering a PIN on the device fires nothing at all. And a guard stops the poll interleaving with a signature, since both share one HID pipe and two conversations over it corrupt each other. The device artwork is Ledger's own. Ledger open-sources Ledger Live, and the Nano S Plus animations ship as Lottie JSON under MIT, so we render the exact animation a Ledger owner already recognises, licence carried alongside the files. THE ASSISTANT, AND WHY IT CANNOT ACT The page-aware assistant sends a typed context envelope rather than a hand-built mega prompt: route, page kind, current revisions, selected entity, explicit attachments. Mentions resolve to real entity IDs in the frontend before the turn is sent, so the model never guesses what @guardian refers to. Responses render as structured cards bound to an authority tier. A proposed patch cannot mutate anything; accepting it hands the proposal to the page that owns the artifact, which shows it as a pending draft change. A control suggestion can only open the product's own typed confirmation dialog. There is no code path from the assistant to a privileged action, which is the same argument the policy layer makes, applied to our own UI. OTHER THINGS WORTH MENTIONING Keeping the wallet stack out of the bundle. RainbowKit pulls roughly 7,000 modules transitively and one connector reaches a package that does not resolve here at all. The wallet session is a tiny context that imports nothing heavy, the real runtime mounts behind a dynamic import only once someone connects, and unused connector subpaths are aliased to an empty module in both the Turbopack and webpack configs. Turbopack matches alias keys as exact specifiers with no prefix matching, so all 22 subpaths had to be listed individually. Every route now ships 135 to 144 kB, including the deploy page, instead of 353 kB. A circular layout bug worth the warning. The signing sheet resizes between steps by measuring the active panel and setting the container height from it. The panels were absolutely positioned with inset: 0, which makes a panel's height equal to its container's, so the measurement returned the value it was about to set and the card could never grow to fit its content. Dropping bottom from the positioning fixed it.