Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions examples/arbitrum-london/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,11 @@ cast send $GATE_ARB "setSpendLimit(uint256)" 1000000000000000000 \
--rpc-url arbitrum_sepolia --private-key $DEPLOYER_PRIVATE_KEY
```

## 3. Deploy to Robinhood Chain testnet
## 3. Deploy to Robinhood Chain testnet (sponsor bonus - real tx on a second chain)

Gate only, no `--verify` (explorer verifier API not published as of 2026-05-26).
Same shape as step 2 but on chainId 46630, no `--verify` (explorer verifier API not published as of 2026-05-26). Deploying the router here too lets `SmokeExecuteEnvelope` (step 7) land a real `executeEnvelope` tx on Robinhood - the only way to get a Robinhood tx hash while scenario C has no UI flow.

### 3a. AgentPolicyGate

```bash
forge script script/DeployRobinhoodTestnet.s.sol \
Expand All @@ -134,7 +136,25 @@ forge script script/DeployRobinhoodTestnet.s.sol \
export GATE_ROBINHOOD=0x... # paste from console
```

No router and no allow-list needed on Robinhood until scenario C's envelope builder ships.
### 3b. MockPendleRouter

```bash
forge script script/DeployMockPendleRouter.s.sol \
--rpc-url robinhood_testnet \
--broadcast
```

```bash
export ROUTER_ROBINHOOD=0x... # paste from console
```

### 3c. Allow-list the router on the gate (REQUIRED)

```bash
cast send $GATE_ROBINHOOD "setAllowedRecipient(address,bool)" $ROUTER_ROBINHOOD true \
--rpc-url robinhood_testnet \
--private-key $DEPLOYER_PRIVATE_KEY
```

## 4. Wire the addresses into the app

Expand Down Expand Up @@ -218,6 +238,29 @@ Open `/flow-a`, ask the agent to prepare a Pendle yield swap. Expect:

That is the recordable Loom path for scenario A.

## 7. Capture real tx hashes (required - AI Agentic category)

The UI sign button already lands a real `executeEnvelope` tx (the recordable Loom moment). For a reproducible, no-wallet artifact - and the only way to land a tx on Robinhood Chain, where scenario C has no UI yet - run the smoke script. It reproduces exactly what the dApp does: the agent signs an envelope, then it executes through the gate. The full path (deploy -> allow-list -> smoke tx) was rehearsed on a local anvil before shipping.

Arbitrum Sepolia:

```bash
cd examples/arbitrum-london/contracts
GATE_ADDRESS=$GATE_ARB ROUTER_ADDRESS=$ROUTER_ARB \
forge script script/SmokeExecuteEnvelope.s.sol --rpc-url arbitrum_sepolia --broadcast
```

Robinhood Chain testnet (bonus):

```bash
GATE_ADDRESS=$GATE_ROBINHOOD ROUTER_ADDRESS=$ROUTER_ROBINHOOD \
forge script script/SmokeExecuteEnvelope.s.sol --rpc-url robinhood_testnet --broadcast
```

The tx hash prints in the broadcast output and is saved to `broadcast/SmokeExecuteEnvelope.s.sol/<chainId>/run-latest.json` under `transactions[0].hash`. Paste both contract addresses and the tx hashes into the "Live on-chain" tables in `examples/arbitrum-london/README.md` - that is the verifiable proof judges check.

The gate enforces the prerequisites on-chain: a signer-pair mismatch reverts `InvalidSignature`, a missing allow-list reverts `RecipientNotAllowed`. If the script succeeds, the demo path is sound.

## Gotchas captured during scaffolding

- `MockPendleRouter` did not exist before 2026-05-29; the app referenced it but the contract + deploy script were missing. They are now in `contracts/src/MockPendleRouter.sol` + `contracts/script/DeployMockPendleRouter.s.sol`.
Expand Down
61 changes: 61 additions & 0 deletions examples/arbitrum-london/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# txKit - Arbitrum London Buildathon demo

**Verify before you sign**, for AI-agent-initiated transactions. A Claude agent turns a
plain-English intent into an [ERC-8265](https://github.com/ethereum/ERCs/pull/1753) Prepared
Transaction Envelope. The user reviews a typed, decoded, fee-previewed summary. An on-chain
`AgentPolicyGate` enforces the policy - recipient allow-list, spend cap, replay protection, and
EIP-712 agent-signer binding - before anything executes.

- **Scenario A (live):** Pendle yield swap on Arbitrum Sepolia.
- **Scenario C (roadmap):** x402-paid RWA agent on Robinhood Chain testnet.

## Live on-chain

Real, verifiable artifacts - the AI Agentic category requires real tx hashes, not preview-only
flows. Machine-readable addresses live in [`contracts/deployed.json`](./contracts/deployed.json);
the tables below are the judge-facing copy, filled in after deploy (see [`DEPLOY.md`](./DEPLOY.md)).

### Arbitrum Sepolia - chainId 421614

| What | Value |
|---|---|
| AgentPolicyGate | [`0x__PENDING__`](https://sepolia.arbiscan.io/address/0x__PENDING__) |
| MockPendleRouter | [`0x__PENDING__`](https://sepolia.arbiscan.io/address/0x__PENDING__) |
| Agent-executed tx (`executeEnvelope`) | [`0x__PENDING__`](https://sepolia.arbiscan.io/tx/0x__PENDING__) |

### Robinhood Chain testnet - chainId 46630 (sponsor bonus)

| What | Value |
|---|---|
| AgentPolicyGate | [`0x__PENDING__`](https://explorer.testnet.chain.robinhood.com/address/0x__PENDING__) |
| MockPendleRouter | [`0x__PENDING__`](https://explorer.testnet.chain.robinhood.com/address/0x__PENDING__) |
| Agent-executed tx (`executeEnvelope`) | [`0x__PENDING__`](https://explorer.testnet.chain.robinhood.com/tx/0x__PENDING__) |

## What this proves

1. An autonomous agent **prepares** a transaction from natural-language intent (Claude tool use
produces an ERC-8265 envelope).
2. The human **sees** a typed, decoded preview - function, arguments, sequencer-fee breakdown,
expiry, policy verdict - before signing, not blind calldata.
3. An on-chain gate **enforces** the rules, so a rogue or compromised agent cannot move value
outside policy. Five checks in `AgentPolicyGate.executeEnvelope`: forwarded value matches the
declared value, envelope not already used, recipient allow-listed, value within the spend cap,
and the EIP-712 signature recovers to the configured agent signer.

The verification layer - not agent autonomy - is the point. It scales to any agent and any
transaction.

## Run it

- **Deploy + capture tx hashes:** [`DEPLOY.md`](./DEPLOY.md) - one funded testnet key, copy-paste.
- **Local dev:** `pnpm dev`, then open `/flow-a`. Until the contracts are deployed the flow runs
in preview mode (a banner explains, and the agent call is skipped so it spends nothing).
- **Contracts:** `cd contracts && forge test` (20 tests).

## Honest scope

- Scenario A is live end to end. The swap target is a deterministic `MockPendleRouter` (flat
1:0.995, no token custody) so the gate path executes on a testnet without funding real input
tokens - real plumbing, mock payload.
- Scenario C's UI is a placeholder; the policy-gate execution is still proven on Robinhood Chain
via the `SmokeExecuteEnvelope` script (same gate, same envelope shape, real tx).
33 changes: 28 additions & 5 deletions examples/arbitrum-london/app/api/agent/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import {
RWA_TOOL_DEFINITION,
preparePendleYieldSwapArgs,
} from '@/src/agent/tools'
import { getAgentPolicyGateAddress } from '@/src/config/deployed'
import {
checkIsAgentPolicyGateDeployed,
checkIsMockPendleRouterDeployed,
getAgentPolicyGateAddress,
} from '@/src/config/deployed'
import { getEnv } from '@/src/config/env'


Expand Down Expand Up @@ -88,6 +92,27 @@ export const POST = async (request: NextRequest) => {
return NextResponse.json({ error: 'messages must be a non-empty array' }, { status: 400 })
}

// Cost guard: skip the model call entirely when the scenario A contracts are
// not live yet. The envelope cannot be built without them, so calling Claude
// here would spend an API request for nothing. The deploy-pending banner
// already tells the user they are in preview mode.
if (scenario === 'pendle') {
const isGateDeployed = checkIsAgentPolicyGateDeployed(ARBITRUM_SEPOLIA_CHAIN_ID)
const isRouterDeployed = checkIsMockPendleRouterDeployed(ARBITRUM_SEPOLIA_CHAIN_ID)
const isScenarioReady = isGateDeployed && isRouterDeployed
if (!isScenarioReady) {
return NextResponse.json(
{
error: 'Contracts not deployed yet',
hint:
'AgentPolicyGate / MockPendleRouter are still placeholder addresses on ' +
'Arbitrum Sepolia. Deploy them (see DEPLOY.md) and update contracts/deployed.json.',
},
{ status: 503 },
)
}
}

let env
try {
env = getEnv()
Expand Down Expand Up @@ -129,7 +154,7 @@ export const POST = async (request: NextRequest) => {
let completion
try {
completion = await anthropic.messages.create({
model: 'claude-opus-4-5',
model: 'claude-haiku-4-5',
max_tokens: 1024,
system: systemPrompt,
tools: [ toolDefinition ],
Expand Down Expand Up @@ -157,15 +182,14 @@ export const POST = async (request: NextRequest) => {
return NextResponse.json({
reply: textReply,
scenario,
milestone: 'phase-1-day-5-tool-use-text-only',
})
}

if (scenario !== 'pendle') {
return NextResponse.json(
{
error: 'RWA scenario not implemented yet',
detail: 'Phase 2 Day 10 wires the RWA envelope builder + Robinhood Chain deploy.',
detail: 'The RWA scenario is not implemented in this build.',
},
{ status: 501 },
)
Expand Down Expand Up @@ -249,6 +273,5 @@ export const POST = async (request: NextRequest) => {
reply: textReply,
envelope: signedEnvelope,
scenario,
milestone: 'phase-1-day-5-tool-use-with-envelope',
})
}
5 changes: 2 additions & 3 deletions examples/arbitrum-london/app/api/decode/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,8 @@ const exampleDescriptors: ReadonlyArray<RegistryDescriptor> = [

const exampleRegistry = buildRegistry(exampleDescriptors)

// Inferred shape matches @txkit/tx-decoder's Registry (Readonly<Record<string, RegistryDescriptor>>).
// We avoid importing the Registry type because it is currently not exported
// from the package barrel - shape-compatibility via spread is sufficient.
// The Registry type is not exported from the @txkit/tx-decoder barrel yet, so
// we merge by spread - the inferred shape is structurally compatible.
const mergedRegistry = {
...BUILTIN_REGISTRY,
...exampleRegistry,
Expand Down
Loading
Loading