How we rebuilt wallet integration from scratch, fixed a currency-conversion bug that silently cancelled every listing, and shipped a live agentic RWA marketplace on Casper Testnet — all in time for the Agentic Buildathon Final Round.
If you've been following the Casper Agentic Buildathon, you know our project Casper Carbon: four Odra smart contracts, three autonomous AI agents (Verifier, Compliance, Market), and a Next.js dashboard — all running live on Casper Testnet.
For the final round, we set out to close the last gap: letting real users buy carbon credits from the marketplace with the Casper Wallet browser extension. What followed was two weeks of debugging, one abandoned SDK, a silent data-corruption bug, and a much better system.
Here is the full technical postmortem.
The Wallet: From 2MB CDN to window.CasperWalletProvider
Phase 1: CSPR.click (why we abandoned it)
Our original wallet integration used CSPR.click, a pre-built UI widget. On paper it was perfect: drop a CDN script tag, add a container div, call showTopBar(), and you get a polished wallet popup.
In practice:
- 2MB CDN payload. The bundle includes the full UI library, SVG assets, and a runtime that renders a React-like virtual DOM inside your page. Every page load stalled while it parsed.
-
MaxListenerswarnings. The SDK attached dozens of event listeners and never cleaned them up. After three page navigations, Node warnedMaxListenersExceededWarning, and the browser tab became sluggish. -
Regex crash on wallet connect. The SDK's internal message parser used a regex that catastrophically backtracked on certain Casper Wallet responses. The page would freeze for 8 seconds, then throw
"Regex execution time exceeded". We filed a minimal reproduction. - Machine heating. With the polling and listener churn, our M3 MacBooks ran the dashboard fan at 5000 RPM just sitting on the marketplace page.
We spent three days trying to isolate the regex crash. Eventually we realized: if the pre-built UI is the problem, cut it out. Talk to Casper Wallet directly.
Phase 2: Direct integration
window.CasperWalletProvider() is available when the Casper Wallet browser extension is installed. The API is simple:
const provider = window.CasperWalletProvider();
await provider.requestConnection();
const publicKey = await provider.getActivePublicKey();
That's it. No CDN, no UI framework, no bundled React.
One subtlety: CasperWalletProvider() takes no arguments — earlier documentation showed passing window.location.origin, but this throws in recent extension versions. Just call it bare.
We rewrote wallet.tsx as a thin React context that wraps these three calls. The entire wallet integration dropped from 2MB to ~40 lines of TypeScript.
// web/src/lib/wallet.tsx (simplified)
const provider = window.CasperWalletProvider();
await provider.requestConnection();
const pk = await provider.getActivePublicKey();
setPublicKey(pk);
Phase 3: Server-side deploy building
Once the user is connected, they need to buy a credit. The flow is:
- User clicks Buy → server builds the unsigned deploy → returns it as JSON
- User signs it in the Casper Wallet extension
- Signed deploy is proxied to the Casper RPC endpoint
The trickiest part was the deploy builder. casper-js-sdk's Deploy class has a toJSON() that is static — Deploy.toJSON(deploy), not deploy.toJSON(). The difference between a silently empty response and a valid deploy.
We also hit the classic CLValue casing trap: the SDK uses newCLUInt256() (capital U, I), while intuition says newCLUint256(). The SDK is inconsistent here across versions — check your typings before you trust auto-complete.
The Market Agent: When "50 Basis Points" Cancelled Everything
One of our core features is the Market Agent, an autonomous bot that compares on-chain credit prices against the live Carbonmark spot price and cancels listings that deviate beyond 50 basis points (bps).
After the qualifier round, the market agent was running, logging "Adjusted 12 listings" every cycle, and we thought it was working perfectly. But the marketplace page showed zero active listings.
Here's the bug:
// agents/src/market.ts (before fix)
const externalPriceMotes = BigInt(
Math.floor(externalPrice * 1_000_000_000)
);
externalPrice comes from Carbonmark in USD per tonne — say, $10. So externalPriceMotes becomes 10_000_000_000.
But the on-chain listing price is in CSPR motes. If the verifier priced at 15 CSPR/tonne and CSPR is $0.10, the fair price should be (10 / 0.10) * 1e9 = 100_000_000_000 motes, not 10_000_000_000.
The market agent was comparing 15_000_000_000 (on-chain) against 10_000_000_000 (USD treated as CSPR), computing a spread of (15e9 - 10e9) / 1e7 = 500 bps, and cancelling every listing because 500 > 50.
Every. Single. One.
The fix was straightforward once we saw it: fetch the CSPR/USD price from CoinGecko and convert properly:
const csprPrice = await fetchCsprUsdPrice(); // e.g., 0.10
const externalPriceMotes = BigInt(
Math.floor((externalPrice / csprPrice) * 1_000_000_000)
);
We also added a fetchCsprUsdPrice() function with caching (60-second TTL) and three retries:
export async function fetchCsprUsdPrice(): Promise<number | null> {
// CoinGecko → "casper-network" → usd
// cached for 60s, retries 3x with 2s backoff
}
Upshot
The market agent now correctly computes spreads. Our listings stay active. The Buy button actually works.
The Verifier: Market-Aware Pricing
While we were at it, we fixed the verifier too. It was hardcoding:
price_per_token: "15000000000" // 15 CSPR, always
Which made no sense once CSPR moved 50% in a week. Now it fetches the same Carbonmark + CoinGecko data and prices at fair market value:
const [carbonPrice, csprPrice] = await Promise.all([
fetchCarbonPrice(config.CARBON_PROJECT_KEY),
fetchCsprUsdPrice(),
]);
const fairMotes = BigInt(
Math.floor((carbonPrice / csprPrice) * 1_000_000_000)
);
pricePerToken = fairMotes.toString();
If either price fetch fails, it falls back to 15 CSPR — degraded but never blocked.
UI Polish: Live Agent Status, Better Error States, Throttled Polling
Agent live indicators
We added green/gray dots to the dashboard for each agent. They work by polling CSPR.cloud's deploy history per agent account: if a deploy was submitted within the last 40 seconds, the dot is green; otherwise gray.
The initial implementation sent a deploy-history request every 10 seconds for each of three agents. On page load with three agents, that was 18 requests/minute to CSPR.cloud. We throttled the poll hook itself:
// usePoll now skips if a request is already in-flight
const inflight = useRef(false);
if (inflight.current) return;
inflight.current = true;
// ... fetch ...
inflight.current = false;
Default intervals: 30s for chain state, 30s for agent activity (up from 10s).
ErrorCard and EmptyState
Every page now wraps its data in consistent error/empty states:
// web/src/components/shared.tsx
export function ErrorCard({ message }: { message: string }) {
return (
<div className="rounded-xl border border-red-500/20 bg-red-500/5 p-6">
<h3>Failed to load data</h3>
<p>{message}</p>
</div>
);
}
No more unhandled promise rejections silently blanking a page.
Verifiable AI hero section
The dashboard now has a dedicated callout explaining the SHA-256 commitment model: every LLM judgment is hash-committed on-chain, and the browser recomputes the hash to verify integrity. Three agent explanation cards below it give a quick visual summary of who does what.
What Didn't Change (And Why)
- Contract logic. The four Odra contracts (AgentRegistry, CarbonProjectRegistry, CarbonCreditToken, CarbonMarketplace) deployed in the qualifier round are untouched. All our fixes were in the TypeScript agent layer and the Next.js dashboard — the contracts just work.
- The compliance agent. It continues monitoring for fraud autonomously, no changes needed.
-
The state decoder. Our reverse-engineered Odra
statedictionary decoder (blake2b256 of field index + mapping key, with manual bytesrepr deserialization) has been reading chain state without a hitch since day one.
Repository
All code is open source at github.com/harishkotra/casper-carbon.
git clone https://github.com/harishkotra/casper-carbon
cd casper-carbon
./demo.sh
Top comments (0)