Lepton Agents Hackathon · Canteen × Circle × Arc
Agents post a bond to register with ArcID. A consumer agent reasons — using Claude — whether the provider delivered. On a confirmed breach, the bond slashes automatically and pays the consumer. Reputation is capital at risk, not a score.
Addresses Lepton's Prior Art #8 (bonded agent reputation) and RFB 3 (agent-to-agent nanopayment networks). The trust layer Prior Art #8 called "nearly empty" — this fills it.
Transparency note: the submission form locked 2026-07-06. Payment execution on a clean verdict (real Circle Gateway settlement +
ArcIDBond.recordSettlement()) was added afterward, during the (extended, ongoing) event window — judges track commit activity through the end of the event, and no winner date had been announced at the time. See CHANGELOG.md for the full breakdown, commit-by-commit.
The complete loop ran end-to-end on Arc testnet on 2026-06-27:
Slash tx: 0xf76fabf96bc7254cca57b41875cf5cf202aa9ae0e44db541297f9b99df8276b6
Block: 48910669 · Chain: Arc testnet (5042002)
Fault: bad-sig — oracle returned a signature with a non-canonical s component (0xcdcdcd...).
Claude's adjudication, written verbatim to the AgentSlashed event on-chain:
The
scomponent (0xcdcdcd...) is non-canonical — it falls in the upper half of the secp256k1 curve order. EIP-2 mandates that valid signatures must use the low-s form. This is not a transient network glitch or an ambiguous failure — it is a deterministic, attributable cryptographic defect in the signature produced by the oracle. The oracle is demonstrably live (fresh timestamp, valid value), so this is not a crash or transient outage. Because the signature fails to recover to the oracle's registered wallet (0xe2F7a0E6d9865C7Dc9B5D19DCc11CBcb4655c661), the oracle cannot prove authorship of this response. The failure is fully attributable to the oracle provider. Verdict: breach. Slashing is justified.
Before / after:
| Before | After | |
|---|---|---|
Oracle bond (0xe2F7a0E6...) |
3.00 USDC · active | 3.00 USDC · slashed |
| ArcIDBond contract balance | 13.00 USDC | 10.00 USDC |
Consumer wallet (0x8F43C6a0...) |
0.92 USDC | 3.92 USDC (+3.00 received) |
Three properties stacked. No competitor, including AOZ, has all three:
| Property | What it means |
|---|---|
| TEE-gated identity | Only wallets with DCAP attestation in ArcIDRegistryV2 can post a bond. An unverified wallet reverts on-chain with "Agent not TEE-verified in ArcID registry". A wrong answer is cryptographically attributable to a real, specific agent. |
| USYC yield-bearing collateral | Bond collateral is USYC — Hashnote's tokenized T-bill fund on Arc. It earns ~4.9% APY while staked. Capital at risk that isn't idle capital. |
| LLM-reasoned adjudication | The consumer agent reasons about why a failure is a breach vs a blip, and writes a rationale that goes on-chain in the AgentSlashed event. Not a cron job. |
AOZ has "bond + slash." ArcID has TEE-gated identity + USYC yield + written LLM rationale on-chain.
The oracle's payment path is wired to Circle's real infrastructure, not a mocked stand-in:
| Component | What's used |
|---|---|
| x402 protocol | GET /api/price is x402-gated — an unpaid call gets a real 402 Payment Required with signed payment requirements, not a stubbed error. |
| Circle Gateway Nanopayments | @circle-fin/x402-batching's createGatewayMiddleware() wraps the endpoint; payments are verified and settled (batched) by Circle's live testnet facilitator (gateway-api-testnet.circle.com). |
| Arc Testnet | Chain ID 5042002 — Circle's own Arc network (eip155:5042002 in Gateway's supported-networks list), using the same USDC precompile (0x3600...0000) as ArcIDBond.sol. |
| Seller Wallet | The oracle's wallet (0xe2F7a0E6d9865C7Dc9B5D19DCc11CBcb4655c661) is the Gateway seller — receives $0.001 USDC per call, checkable live via GET /api/gateway-balance. |
The frontend's "Circle Gateway Nanopayment" card pays for one real /api/price call and shows the seller's Gateway balance before → after.
# 1 — Oracle (x402-gated, signs responses, serves fault modes)
cd oracle && npm install && npm start # http://localhost:3001
# 2 — Consumer agent (pays oracle, adjudicates, slashes on breach)
cd consumer && npm install && npm start # runs continuously
# 3 — Dashboard (live traction strip, fault injection, verdict feed)
cd frontend && npm install && npm run dev # http://localhost:5174Trigger a fault live (dashboard → AgentCard → "stale"): Consumer detects breach within ~12s, Claude writes the rationale, slash fires.
# Contracts (50 tests, no external RPC)
npm test
npm run deploy:standalone:local # deploy DCAPVerifier + ArcIDRegistryV2 + ArcIDBond,
# generate DCAP quote, register + bond in one command
npm run gating:local # proof-of-gating revert outputAll commands read contract addresses from deployments/arcTestnet_standalone.json
(written by npm run deploy:standalone). Add --network hardhat to target a local
Hardhat node instead. All commands default to Arc testnet.
npm run agent:register -- --key <private-key>Builds a fresh DCAP attestation quote for the given wallet, calls
ArcIDRegistryV2.registerAgent(), and prints the resulting agentId. Idempotent —
re-running with the same key prints the existing ID and exits.
→ Registering 0x71bE...abc on arcTestnet
ArcIDRegistryV2: 0x...
→ Building DCAP attestation quote for 0x71bE...abc...
reportData: 0xdeadbeef...
✓ registerAgent() mined → 0xabc123...
agentId: 0xbeefdead...
tx block: 14209543
npm run bond:post -- --key <private-key> [--amount 5.0]Requires the wallet to already be registered. Approves USDC, calls postBond(),
and prints the resulting bond status. --amount is in whole USDC (default: 5.0).
→ Posting 5.0 USDC bond from 0x71bE...abc on arcTestnet
agentId: 0xbeefdead... ✓
→ Approving 5.0 USDC...
→ Calling postBond(5000000)...
✓ postBond() mined → 0xdef456...
amount: 5.00 USDC
posted at: 2026-06-26 11:04:12 UTC
active: true
npm run agent:status -- --address <wallet-address>Read-only. No private key required.
→ Status for 0x71bE...abc on arcTestnet
Registry 0x...
registered: yes ✓
agentId: 0xbeefdead...
Bond 0x...
status: active ✓
amount: 5.00 USDC
posted at: 2026-06-26 11:04:12 UTC
npm run agent:listQueries AgentRegistered events from the deployment block forward and cross-references
bond status for each address. Read-only. Add --from-block <n> if the RPC limits query range.
→ Listing agents on arcTestnet (from block 14209540)
# Address AgentId (prefix) Bonded Amount
──── ──────────────────────────────────────────── ────────────────────── ──────── ────────────
1 0x71bE...abc 0xbeefdead... yes ✓ 5.00 USDC
2 0xF3a9...12c 0xdeadbeef... slashed 10.00 USDC
Total: 2 agents
npm run bond:slash -- \
--key <slasher-private-key> \
--agent <agent-address> \
--consumer <consumer-address> \
--reason "Stale data: response was 90s past the 30s SLA window"Caller must be the authorizedSlasher on ArcIDBond. The --reason string goes
on-chain verbatim in the AgentSlashed event, same as the consumer agent's LLM rationale.
→ Slashing agent 0x71bE...abc on arcTestnet
Bond amount: 5.00 USDC
Reason: "Stale data: response was 90s past the 30s SLA window"
✓ slash() mined → 0xabc999...
5.00 USDC transferred to consumer 0xF3a9...12c
npm run bond:settle -- \
--key <slasher-private-key> \
--agent <agent-address> \
--consumer <consumer-address> \
[--amount 0.001] \
[--verdict-hash 0x...]The "no breach" counterpart to bond:slash. Caller must be the
authorizedSlasher. Does not move funds — the Circle Gateway payment already
happened (via the consumer agent's settlement.js, or manually); this only
writes the on-chain PaymentSettled audit record. Reverts (AlreadySlashed)
if the agent's bond was already slashed, so a payment can never be logged
against an agent that was already paid out via slash.
→ Recording settlement for agent 0x71bE...abc on arcTestnet
ArcIDBond: 0x...
Caller: 0x...
Consumer: 0xF3a9...12c
Amount: 0.001000 USDC
VerdictHash: 0x9d5be3aa...
✓ recordSettlement() mined → 0x1304d8e4...
0.001000 USDC settlement logged for agent 0x71bE...abc
npm run gating:check -- --key <private-key>If the wallet is not registered: performs a staticCall to postBond() (zero gas
cost) and confirms the exact revert message. If registered: reports its agentId.
→ Gating check for 0xRand...om on arcTestnet
Wallet is NOT registered — confirming gating revert via staticCall...
✓ GATING CONFIRMED
Revert: "Agent not TEE-verified in ArcID registry"
To register: npm run agent:register -- --key 0x... --network arcTestnet
┌──────────────────────────────────────────────────────────┐
│ DCAPVerifier (on-chain, Arc testnet) │
│ verify(quote, sig) → (ok, {mrtd, reportData, signer}) │
│ Checks TDX v4 header + ecrecover on report_data sig │
└─────────────────────┬────────────────────────────────────┘
│ verification
┌─────────────────────▼────────────────────────────────────┐
│ ArcIDRegistryV2 (native) │
│ registerAgent(dcapQuote, sig) → on-chain registration │
│ agentIdBySigner[wallet] → bytes32 agentId │
└─────────────────────┬────────────────────────────────────┘
│ gating check
┌─────────────────────▼────────────────────────────────────┐
│ ArcIDBond (Phase 1) │
│ postBond(amount) → reverts for unverified wallets │
│ slash(agent, consumer, reason) → transfers bond │
│ isActiveBondedAgent(addr) → bool │
│ Events: BondPosted · AgentSlashed · BondWithdrawn │
└─────────────────────┬────────────────────────────────────┘
│ x402 nanopayments
┌─────────────────────▼────────────────────────────────────┐
│ Oracle Service (Phase 2) │
│ GET /api/price — $0.001 via Gateway nanopayments │
│ Response: {value, timestamp, signature} │
│ Fault modes: stale / null / bad-sig (for demo) │
└─────────────────────┬────────────────────────────────────┘
│ LLM adjudication
┌─────────────────────▼────────────────────────────────────┐
│ Consumer Agent (Phase 3) ⭐ │
│ Calls oracle → pays x402 → verifies signature │
│ LLM judge: did the provider meet its SLA? │
│ Verdict: {verdict: "breach", reason: "47s stale..."} │
│ On breach → slash() → proceeds to consumer wallet │
└──────────────────────────────────────────────────────────┘
This is an explicit statement of a split that already exists in the code, written down so it reads as an intentional design decision rather than an omission once payment execution (below) is built on top of it.
What actually runs inside the TDX enclave: exactly one thing — proving that a
given wallet key belongs to code running inside a genuine TDX CVM with a specific
measured code identity (mrtd). That's GET /api/attest in oracle/src/attest.js:
when USE_REAL_PHALA=true it calls the Phala dstack guest agent over its Unix
socket to produce a real hardware-backed DCAP quote; the resulting quote is what
ArcIDRegistryV2.registerAgent() verifies on-chain via DCAPVerifier. This is the
entire hardware root of trust in the system.
What runs outside it, as ordinary application code:
- Price signing (
GET /api/price) — the oracle signs(value, timestamp)with the same wallet key, but the signing call itself isn't re-attested per request. Trust here is transitive: the wallet was proven TEE-resident once at registration time, so signatures from it are attributable, not because every signature re-enters the enclave. - LLM adjudication — Phase 3's consumer agent runs as a plain local Node process, never deployed to Phala. Claude's verdict reasoning has no TEE involvement at all.
- x402 / Circle Gateway payments — both
createGatewayMiddleware()(oracle-side,oracle/src/index.js) andGatewayClient(payForPriceViaGateway()inoracle/src/chain.js, used by/admin/demo-pay) are plain ethers/Node code paths. Even the Gateway call that happens to execute inside the oracle's own container never touches the dstack socket — onlyattest.jsdoes. This is the same "sidecar" relationship the Gateway payment code already has to the x402-gated route it sits next to: adjacent in the process, but a separate trust tier.
Why this matters going forward: any new payment-execution logic (settlement calls triggered by a verdict) belongs in this second tier by construction — it should live in the consumer agent's post-verdict handler, not inside the oracle, and it inherits trust from the already-attested identity and the on-chain bond/slash contract, not from any new TEE involvement of its own.
| Phase | What | Status |
|---|---|---|
| 1 | ArcIDBond.sol — TEE-gating, slash, 27 tests |
✅ Complete |
| 2 | Oracle service — x402 nanopayment endpoint, 3 fault modes | ✅ Complete |
| 3 | Consumer agent — Claude adjudication, slash loop | ✅ Complete |
| 4 | Frontend — live traction strip, fault injection, verdict feed | ✅ Complete |
| 5 | USYC yield-bearing collateral — Teller, 13 tests, deploy scripts | ✅ Complete |
| 6 | Video script, submission form, checklist | ✅ Complete → SUBMISSION.md |
| 7 | ArcIDRegistryV2.sol + DCAPVerifier.sol — native on-chain registry with real DCAP verification; deploy:standalone registers + bonds in one command; 10 new tests |
✅ Complete |
| Post-submission | Payment execution — real Circle Gateway settlement + ArcIDBond.recordSettlement() audit trail on a clean verdict; 7 new tests |
✅ Complete → CHANGELOG.md |
Test suite: 57 passing (npm test) — no external RPC, no .env required.
The Circle-specific moat: ArcIDBond.sol already supports any ERC-20 — the same contract deployed with the USYC token address gives you yield-bearing bonds. No new contract code.
What was built:
| Artifact | Description |
|---|---|
contracts/mocks/MockUSYC.sol |
8-decimal yield-bearing mock; simulateYield(bps) advances share price |
contracts/interfaces/ITeller.sol |
Interface for Arc testnet Teller (deposit / redeem / sharePrice) |
test/ArcIDBondUSYC.test.js |
13 tests telling the yield story end-to-end |
scripts/deploy_usyc.js |
Deploy ArcIDBond with USYC; handles allowlist absence gracefully |
scripts/mint_usyc.js |
Mint USYC from USDC via Teller on Arc testnet |
frontend/src/components/USYCBondCard.jsx |
Purple "yield-bearing" card with narrative + deployed contract address |
Test suite highlights (npm test — 57 passing total):
USYC bond face value is $5.00 USDC at deposit time (sharePrice = $1.00)
bond value increases as USYC share price accrues yield
→ 5 USYC * $1.005 = $5.025 after 50 bps yield
491 bps (~4.9% APY) yields the correct appreciated bond value
consumer's USYC is worth more than the original $5.00 bond face value
→ 5 USYC * $1.02 = $5.10 after 200 bps while bonded
agent receives USYC back on withdrawal (yield value is captured on Teller redeem)
two USYC bonds coexist; yield accrues on both
The narrative the tests prove:
Bond earns T-bill yield (~4.9% APY) while at stake. On a confirmed breach, the consumer receives USYC that has already appreciated — more than face value. Capital at risk that isn't idle capital.
Deploy to Arc testnet:
# Step 1 — get USYC (requires Circle allowlist)
npm run mint:usyc:arc
# Step 2 — deploy ArcIDBond with USYC collateral
npm run deploy:usyc:arc
# → handles allowlist-absent case: still deploys + prints contract addressUSYC addresses on Arc testnet:
| Contract | Address |
|---|---|
| USYC token | 0xe9185F0c5F296Ed1797AaE4238D26CCaBEadb86C |
| Teller (mint/redeem) | 0x9fdF14c5B14173D74C08Af27AebFf39240dC105A |
If allowlist hasn't arrived: deploy:usyc:arc still deploys the contract. Judges can inspect the code and TEE-gating at the deployed address. The bond is posted once allowlist access is confirmed.
A live React dashboard (Vite 5, port 5174) that visualises system health in real-time without any manual curl commands — the single-pane-of-glass for a hackathon demo.
Five-panel layout:
| Panel | What it shows |
|---|---|
| Traction strip | Bonded agents · Total calls · Volume (USDC) · OK verdicts · Slash events |
| Agent card | Oracle wallet address · active/slashed badge · collateral · fault-injection buttons |
| Fault controls | One-click stale / null / bad-sig buttons calling POST /admin/fault — viewer can watch slash appear live |
| System info | Chain, protocol, TEE gate, adjudicator, consumer wallet |
| Verdict feed | Scrolling history of every Claude adjudication: badge (ok/breach/uncertain), three check marks, LLM rationale, payment amount, age |
The dashboard polls /api/stats and /api/verdicts every 5 seconds and shows a live/disconnected indicator. All API calls are proxied through Vite to the oracle (no CORS in production build).
Start:
cd frontend
npm install
npm run dev # http://localhost:5174Oracle API extensions (added for Phase 4):
| Endpoint | Purpose |
|---|---|
GET /api/stats |
Traction counters + oracle/consumer addresses |
GET /api/verdicts |
Last 50 adjudications (newest first) |
POST /api/verdicts |
Consumer agent pushes verdict after each cycle |
POST /admin/fault |
Set fault mode (stale / null / bad-sig) |
POST /admin/fault/reset |
Clear fault mode |
Consumer agent now sends consumer address in every verdict POST so the dashboard can surface it.
An autonomous loop that pays for oracle data, verifies it, and lets Claude reason about whether the provider delivered — then slashes on a confirmed breach.
Adjudication results (live-tested):
| Fault mode | Age | Sig | Value | Claude's verdict | Slash? |
|---|---|---|---|---|---|
| none (healthy) | 0s | ✓ | ✓ | ok — "No SLA violations. No slashing warranted." | No |
stale |
90s | ✓ | ✓ | breach — "Oracle is provably live yet served data 3× past the SLA. Signed and attributable → slashable." | Yes |
null |
0s | null | null | uncertain — "Single isolated null; timestamp is fresh so oracle is live. Benefit of the doubt on Cycle 1." | No |
bad-sig |
0s | ✗ | ✓ | breach — "Non-canonical s value is a deterministic signing failure, not a network blip. Authorship cannot be proven." | Yes |
The uncertain verdict on null demonstrates adjudicator restraint — the agent correctly refuses to slash on ambiguous failures. This is explicitly part of the agency story.
Run locally (oracle must be running first):
# Terminal 1
cd oracle && npm start
# Terminal 2 — normal loop
cd consumer && npm start
# Single cycles for each fault mode
npm run fault:stale
npm run fault:null
npm run fault:bad-sigLog format (consumer/logs/*.jsonl):
{"cycle":1,"verdict":"breach","reason":"...LLM rationale...","checks":{"timestamp_fresh":false,"value_present":true,"signature_valid":true},"payment_usdc":0.001,"slash_tx":"0xf76fabf9..."}Every line is traction data: cycle count, payment volume, slash count, LLM rationale, on-chain tx hash.
A nanopayment-gated Express service that signs every response with the oracle agent's registered wallet. Three distinct fault modes let Phase 3's consumer agent reason about different breach types.
Response format:
{
"value": "3450.12",
"timestamp": 1782416932,
"oracle": "0xe2F7a0E...",
"signature": "0xfd7363...",
"sla": { "max_age_seconds": 30 }
}Signature: sign(keccak256(abi.encodePacked(string(value), uint256(timestamp)))) — consumer verifies with ethers.verifyMessage. Wrong answers are attributable to the oracle's TEE-registered wallet.
Fault modes (distinct inputs to Phase 3's LLM reasoner):
| Mode | What happens | Phase 3 verdict |
|---|---|---|
?fault=stale |
Timestamp 90s old, valid signature | "Provider live but serving stale data → slashable" |
?fault=null |
value: null, signature: null |
"Malformed response — crash or intentional? Check if recurring" |
?fault=bad-sig |
Valid value + timestamp, corrupted signature | "Cannot verify authorship → slashable" |
Start locally:
cd oracle
cp .env.example .env # fill in ORACLE_PRIVATE_KEY + ORACLE_WALLET_ADDRESS
npm install
npm start # port 3001Test with curl:
# 402 without payment
curl http://localhost:3001/api/price
# 200 with dev payment header
curl -H "X-Payment: dev" http://localhost:3001/api/price
# Fault modes
curl -H "X-Payment: dev" "http://localhost:3001/api/price?fault=stale"
curl -H "X-Payment: dev" "http://localhost:3001/api/price?fault=null"
curl -H "X-Payment: dev" "http://localhost:3001/api/price?fault=bad-sig"x402 in production: set DEV_MODE=false in .env — the service uses @circle-fin/x402-batching's createGatewayMiddleware() to verify and settle real USDC payments via Circle Gateway's live testnet facilitator. DEV_MODE=true (default) accepts any X-Payment header for local testing.
- TEE-gating:
postBond()callsregistry.agentIdBySigner(msg.sender)— reverts with"Agent not TEE-verified in ArcID registry"for unverified wallets. This is the proof-of-gating screenshot. - Bond collateral: fixed ERC-20 at deploy (USDC for Phase 1, USYC for Phase 5 — same contract, different constructor arg).
- Slash:
authorizedSlasher(initially deployer / consumer wallet) callsslash(agent, consumer, reason). Thereasonfield holds the LLM-authored rationale from Phase 3's adjudication agent — logged on-chain in theAgentSlashedevent. - Events for live counters:
BondPosted,AgentSlashed,BondWithdrawn— the frontend reads these for TVL, nanopayment count, and slash history.
postBond → success for verified agent
→ gating revert: "Agent not TEE-verified in ArcID registry"
→ ZeroAmount error
→ BondAlreadyActive error
→ allows re-bond after slash
slash → transfers full bond to consumer
→ emits AgentSlashed with LLM rationale
→ NotAuthorizedSlasher error
→ NoBondFound error
→ AlreadySlashed error
withdrawBond → returns bond to agent
→ emits BondWithdrawn
→ NoBondFound / AlreadySlashed errors
views → isActiveBondedAgent correct across lifecycle
admin → setAuthorizedSlasher (owner only, emits SlasherUpdated)
| Contract | Address |
|---|---|
| DCAPVerifier | 0xBB2835fC4d189340a98084A50DD0B36b4Ff50Ca2 |
| ArcIDRegistryV2 | 0xf1ad81B9FcB805BB75f3c92B5Db67641B7C729C9 |
| ArcIDBond (USDC collateral) | 0xE4860b98AFace0166dD323D0E0b12e680d61D59c |
| ArcIDBond (USYC collateral) | (run npm run deploy:usyc:arc) |
| USDC (Arc testnet) | 0x3600000000000000000000000000000000000000 |
| USYC token | 0xe9185F0c5F296Ed1797AaE4238D26CCaBEadb86C |
| USYC Teller (mint/redeem) | 0x9fdF14c5B14173D74C08Af27AebFf39240dC105A |
Registered & bonded agents (Arc testnet, live state):
| Address | Bond | Status |
|---|---|---|
0x8F43C6a0062D33585d97A54d7f380bc6D52B5440 |
5.00 USDC | Active ✓ |
0xEF5adE59183CAd6A2dDC896BE7f8bE58eDf5f993 |
5.00 USDC | Active ✓ |
0xe2F7a0E6d9865C7Dc9B5D19DCc11CBcb4655c661 |
3.00 USDC | Slashed → tx |
- Node.js ≥ 18
- An Arc testnet wallet funded with USDC (faucet.circle.com)
- Set
DEPLOYER_PRIVATE_KEYin.env(used for transactions and DCAP quote signing)
cd arcid2
npm installnpm run compilenpm test
# or for verbose output:
npx hardhat test --verbosenpm run deploy:standalone:localDeploys DCAPVerifier + ArcIDRegistryV2 + MockUSDC + ArcIDBond, generates a
prototype DCAP quote, registers the deployer on-chain, posts a 5 USDC bond, and
confirms the gating revert — all in ~5 seconds, no external RPC or .env required.
- Copy
.env.exampleto.envand fill inDEPLOYER_PRIVATE_KEY. - Run:
npm run deploy:standaloneThis deploys ArcIDRegistryV2 (pointing at the live DCAPVerifier), generates a
structurally valid TDX quote signed by your wallet, registers it on-chain, deploys
ArcIDBond, and posts a bond — fully self-contained, single command.
npm run gating:arc
# → prints "Agent not TEE-verified in ArcID registry" revert for a random walletconstructor(address _dcapVerifier)| Function | Description |
|---|---|
registerAgent(bytes dcapQuote, bytes reportDataSig) |
Submit a TDX DCAP v4 quote + 65-byte sig. Calls the on-chain verifier; reverts if the quote fails or if ecrecover(reportData, sig) ≠ msg.sender. On success writes agentIdBySigner[msg.sender] = keccak256(mrtd, reportData, signer). |
agentIdBySigner(address) |
Returns the agent's bytes32 id, or bytes32(0) if unregistered. Read by ArcIDBond.sol for the gating check. |
function verify(bytes calldata quote, bytes calldata reportDataSig)
external pure returns (bool ok, QuoteSummary memory summary)Checks TDX v4 header structure, mrtd non-zero, and ecrecover(reportData, sig) for a valid signer. Returns ok = false on any failure (no revert) so callers can gate on the bool.
constructor(address _collateralToken, address _registry)| Param | Value (Arc testnet) |
|---|---|
_collateralToken |
0x3600000000000000000000000000000000000000 (USDC) |
_registry |
ArcIDRegistryV2 address (from deploy:standalone output) |
| Function | Who | Description |
|---|---|---|
postBond(uint256 amount) |
TEE-verified agent | Transfers collateral to contract. Reverts for unverified wallets. |
slash(address agent, address consumer, string reason) |
authorizedSlasher | Transfers bond to consumer. reason is the LLM rationale. |
recordSettlement(address agent, address consumer, uint256 amount, bytes32 verdictHash) |
authorizedSlasher | Post-submission (see CHANGELOG.md). Does NOT move funds — logs an off-chain Gateway settlement against a clean verdict. Reverts if the bond is already slashed. |
withdrawBond() |
Bond holder | Returns unslashed bond to agent. |
isActiveBondedAgent(address) |
view | True if agent has active (unslashed) bond. |
setAuthorizedSlasher(address) |
owner | Rotate the consumer agent wallet. |
| Event | When |
|---|---|
BondPosted(agent, amount, token) |
Successful postBond() |
AgentSlashed(agent, consumer, amount, reason) |
Successful slash() |
PaymentSettled(agent, consumer, amount, verdictHash) |
Successful recordSettlement() — post-submission (see CHANGELOG.md) |
BondWithdrawn(agent, amount) |
Successful withdrawBond() |
SlasherUpdated(oldSlasher, newSlasher) |
setAuthorizedSlasher() called |
| Criterion (weight) | How ArcID v2 addresses it |
|---|---|
| Agentic Sophistication (30%) | Consumer agent uses Claude tool_use with forced structured output to reason about why a failure is a breach vs a transport blip. Three fault modes give genuinely different reasoning paths. Written rationale logged on-chain. uncertain verdict on ambiguous failures demonstrates restraint — the agent knows when not to slash. |
| Traction (30%) | DCAP-verified agents registered on-chain via ArcIDRegistryV2; real x402 nanopayment volume on Arc testnet; outside participants recruited. Every cycle is logged to JSONL — traction is auditable, not claimed. |
| Circle Tool Usage (20%) | x402 Gateway: oracle charges $0.001/call, consumer pays autonomously. USYC collateral: bond deployed with Hashnote's yield-bearing token; Teller integration for USDC→USYC mint. Both used together. |
| Innovation (20%) | First system where TEE-attested identity gates the bond before stake (not stake as identity), and where bond collateral earns T-bill yield while at risk. No adjacent project has both properties. |
The oracle service is containerized and ready to deploy to a Phala TDX CVM, producing a real Intel TDX quote from /api/attest instead of a self-signed prototype.
GET /api/attest is a new endpoint on the oracle. When USE_REAL_PHALA=true:
- Computes
report_data = keccak256(oracle_wallet_address)(32 bytes) - Signs it raw with the oracle private key (no EIP-191, matching
DCAPVerifier._recover()) - Calls the Phala dstack guest agent via
@phala/dstack-sdk'sDstackClient, which connects over the agent's Unix domain socket — not TCP/HTTP. The socket must be volume-mounted into the container (seeoracle/docker-compose.phala.yml); the agent isn't reachable at any address otherwise, regardless of127.0.0.1vs. the container's host. - Returns
{ quote, report_data, report_data_sig, attested_signer, mrtd, real_tdx: true }
When USE_REAL_PHALA=false (default, local dev), the same endpoint returns a structurally-valid TDX v4 prototype quote — same format, self-signed, passes DCAPVerifier on-chain.
| File | Purpose |
|---|---|
oracle/Dockerfile |
Node 18 alpine, linux/amd64, exposes port 3001 |
oracle/src/attest.js |
Attestation logic — real Phala path (dstack Unix socket via @phala/dstack-sdk) + prototype fallback |
oracle/.env.example |
Documents USE_REAL_PHALA |
# 1. Build for linux/amd64
docker build --platform linux/amd64 -t kkoci/arcid2-oracle:latest oracle/
# 2. Push to Docker Hub
docker push kkoci/arcid2-oracle:latest
# 3. Deploy to Phala Cloud
# Dashboard: https://cloud.phala.network/dashboard/cvm
# → "Deploy CVM" → Docker image: kkoci/arcid2-oracle:latest
# → Port mapping: 3001
# → Environment variables (from oracle/.env, plus):
# USE_REAL_PHALA=true
# PORT=3001
# → Compose must volume-mount the dstack guest agent's Unix socket (see
# oracle/docker-compose.phala.yml) — it's not reachable over TCP/HTTP# CVM URL format: https://<hash>-3001.dstack-pha-prod5.phala.network/
# Smoke test — confirm real quote comes back:
curl https://<cvm-hash>-3001.dstack-pha-prod5.phala.network/api/attest | jq .real_tdx
# Expected: true
# The quote field is a hex-encoded TDX DCAP v4 quote (≥592 bytes).
# Pass it to ArcIDRegistryV2.registerAgent() along with report_data_sig.- Decentralized multi-slasher: dispute window + N-of-M slasher quorum (intentionally out of scope for hackathon cadence)
- Broker agent: chooses which bonded provider to route to based on bond size + slash record (Phase 3 stretch)
- USYC redemption flow: yield tracking per bond via Teller
- Live frontend: frontend-five-eta-43.vercel.app
- Arc testnet explorer: testnet.arcscan.app
- Lepton submission: forms.gle/SMqLaw2pMGDe58LFA