The Swing

A Reputation-Gated Credit Layer for Autonomous AI Agents on Mantle — The Complete Engineering Reference
Product Spec · Architecture · Code Walkthrough
Turing Test Hackathon 2026 · Mantle × Bybit × Byreal × BGA · Agentic Economy track
earn → reputation → credit → deploy → rogue-reject
Live · theswing.vercel.app  ·  github.com/frankolien/swing

Contents

Part I — The Thesis
1
What The Swing Is, and Why It Wins
Part II — The On-Chain Spine
2
TierMath: The Reputation → Credit Curve
3
The ReputationOracle
4
CreditVault & CreditManager
5
The SpendingGuard and the Rogue-Reject
6
Deployment, Foundry & Verification
Part III — The Reputation Engine
7
The Scoring Core
8
The Oracle Bridge & Chain Client
9
The HTTP API Surface
10
Live Spend & the On-Chain Rogue-Reject
11
x402 Agent-to-Agent Payments
12
Z.ai GLM Explanations
13
Byreal Context & the On-Chain PnL Indexer
14
Operator Onboarding
Part IV — The Frontend
15
The Design System
16
The Pages
17
The Components
18
Wallet-Connected Product Flows
Part V — Reference
A
The File Map
B
Deployed Addresses & Environment
C
The 90-Second Demo Run-of-Show
D
Glossary
1
Part I — The Thesis

What The Swing Is, and Why It Wins

What you will learn in this chapter

Autonomous agents are starting to create real, verifiable on-chain value — a RealClaw bot trading on Mantle, an agent earning fees over x402. But they have no portable creditworthiness and no safe way to be trusted with capital. A profitable agent cannot borrow against its track record. A compromised agent key can drain a wallet. Identity exists (ERC‑8004) and payments exist (x402), but nothing binds them into an economic trust layer.

The swing is that layer. An agent earns a portable, recency-decayed, Sybil-gated reputation from its real on-chain track record. That reputation gates access to capital: larger, partially- or under-collateralized credit lines drawn from an ERC-4626 lender vault. The capital is deployed through a spending-controlled smart account whose guard rejects rogue transactions on-chain — per-tx caps, rolling daily limits, allowlists. Agents pay each other for services over x402, and proof-of-payment loops back as reputation.

The four pieces

LayerWhat it isWhere it lives
IdentityERC-8004 agentId per agent; agentWallet → the smart accountReputationOracle keys
ReputationOff-chain engine scores PnL + signals → R ∈ [0,1000], committed on-chain with an evidence hashapps/engine + ReputationOracle
CreditERC-4626 lender pool + a CreditManager opening reputation-tiered linescontracts/
SafetyOne Solidity guard library, used by both a direct GuardedAccount and ERC-7579 modulescontracts/src
Paymentsx402 agent-to-agent settlement; proof-of-payment loops back as feedbackapps/engine
ReasoningZ.ai GLM narrates each credit decision in plain languageapps/engine

The loop

Trade
Byreal / RealClaw,
real DEX swaps
Reputation Engine
recency-decayed,
Sybil-gated R
Oracle
commit R + tier
on-chain
CreditManager
tier → limit,
draw from vault
Guarded Account
spends within
policy
✓ allowed tx mines
status 1
✗ rogue tx REVERTS
status 0, on-chain
Figure 1.1 — The north-star loop. Every box is a verifiable Mantle event: a reputation commit, a credit decision, a spend, or a mined revert.

The signature beat is the last box. A compromised key tries to drain the guarded account to a non-allowlisted address; the spend is rejected inside the EVM and the transaction mines as a reverted, status-0 transaction on the Mantle explorer. This is the difference between telling an agent not to misbehave and making it impossible.

Why it wins the rubric

The Agentic Economy track rewards integration depth, agent autonomy, verifiability, and demo quality. The swing maximises all four at once: it uses ERC-8004 (identity), Byreal/RealClaw (the track requirement), x402 (the payments rail the EIP points to), AA-style spending controls (a visceral safety story), and Mantle DeFi — with every score, credit decision, and rejection landing as an on-chain event. It is infrastructure, not a single bot.

Status

Live on Mantle Sepolia (chainId 5003): all 7 spine contracts deployed and source-verified on Mantlescan, 60 passing Foundry tests, 7 passing engine scoring tests. The full north-star loop has been executed on-chain with real transactions — reputation commit (R=832 → T3, via a 2-of-3 committee), credit draw (5,000 USDC), allowed spend (status 1), and a rogue spend that reverted on-chain (status 0). On top of the spine sit two real product entry points — lend on /vault and launch an agent on /launch — both wallet-connected and user-signed.

The rest of this book walks the codebase from the chain up: the contracts that are the source of truth, the engine that projects reputation onto them, and the frontend that turns the whole loop into something a judge can watch happen.

2
Part II — The On-Chain Spine

TierMath: The Reputation → Credit Curve

What you will learn in this chapter

Foundry contracts are the spine; the engine and dashboard are projections of them. The most important consequence of that decision lives in one pure library: TierMath.sol. It defines the entire economic relationship between a number (an agent's reputation R) and a permission (how much capital it can touch, how fast, and against how much collateral). Get this curve right and the rest of the protocol is bookkeeping.

Four tiers

A score in [0,1000] maps to one of four tiers. The boundaries are fixed in tierOf(uint16 score):

TierScore rangeStanceCollateral factorPer-tx capDaily limitTier cap
T0 Unproven0–249over-collateralized150%$25$50$100
T1 Emerging250–499fully collateralized100%$250$500$1,000
T2 Established500–74950% uncollateralized50%$2,000$5,000$10,000
T3 Trusted750–1000uncollateralized20%$10,000$25,000$50,000

All amounts are 6-decimal USDC. The collateral factor is the borrowing-power multiplier the CreditManager applies; a lower factor means the same collateral unlocks more credit (and at T3, credit is granted with no collateral at all).

The super-linear curve

Within a tier’s band, the credit limit grows faster than linearly in reputation, so a great agent is meaningfully rewarded over a merely good one — but it is clamped to the tier cap, so growth is never unbounded:

// TierMath.sol — pure, mirrored in packages/shared
uint256 constant BASE_LIMIT = 50_000e6;   // ceiling, 6-decimal USDC
int256  constant GAMMA_WAD  = 1.5e18;     // the exponent in (R/1000)^1.5
uint16  constant SCORE_MAX  = 1000;

function creditLimit(uint16 score) internal pure returns (uint256) {
    // limit = BASE_LIMIT * (score / 1000) ^ 1.5, then clamp to tierCap(tier)
    uint256 raw  = _powWad(score, GAMMA_WAD) * BASE_LIMIT / 1e18;
    uint256 cap  = tierCap(tierOf(score));
    return raw < cap ? raw : cap;
}

Why 1.5 and not 2.0 or 1.0? A linear curve under-rewards the top of each band; a quadratic curve makes the jump from “good” to “excellent” so steep it dwarfs the tier structure. 1.5 is the compromise that keeps the tier the hard ceiling while still paying a real premium for reputation inside it. An agent at R=832 has borrowing power around $37,945 — the exact number the on-chain demo draws against.

One curve, two languages

The same math lives in packages/shared as a TypeScript mirror so the engine and the dashboard can show a limit before a transaction lands, and the contract can enforce the identical number when it does. The shared package ships a generated set of ABIs and a tier-math mirror; if the Solidity changes, the mirror is regenerated. The contract is always the authority — the mirror exists only to make the UI honest.

3
Part II — The On-Chain Spine

The ReputationOracle

What you will learn in this chapter

The heavy lifting of reputation — recency decay, Sybil gating, cohort normalization — happens off-chain in the engine (Chapter 7). The ReputationOracle is the cheap, trustless read of the result. The engine computes R, then commits it here so that the CreditManager and the dashboard can read a single number without re-running the model.

struct Reputation {
    uint16  score;         // R in [0,1000]
    uint8   tier;          // 0..3, derived via TierMath.tierOf at commit time
    uint64  updatedAt;     // block.timestamp of last commit
    uint32  epoch;         // monotonic; increments every recompute
    bytes32 evidenceHash;  // keccak of the exact scoring inputs
}

// k-of-n committee: needs `threshold` signatures from distinct authorized signers over a
// digest bound to (chainId, this oracle, agentId, score, evidenceHash, nextEpoch).
function commit(uint256 agentId, uint16 score, bytes32 evidenceHash, bytes[] calldata sigs)
    external                                   // permissionless to submit
{
    require(score <= SCORE_MAX, "score");
    uint32 nextEpoch = _rep[agentId].epoch + 1;
    bytes32 ethHash = toEthSignedMessageHash(_digest(agentId, score, evidenceHash, nextEpoch));

    address last; uint256 valid;               // recovered signers must strictly ascend (dedup)
    for (uint256 i; i < sigs.length; ++i) {
        address rec = ECDSA.recover(ethHash, sigs[i]);
        require(rec > last && isSigner[rec]);   // sorted + authorized
        last = rec; ++valid;
    }
    require(valid >= threshold);                // quorum reached

    Reputation storage r = _rep[agentId];
    r.score = score; r.tier = TierMath.tierOf(score);
    r.updatedAt = uint64(block.timestamp); r.epoch = nextEpoch; r.evidenceHash = evidenceHash;
    emit ReputationCommitted(agentId, score, r.tier, nextEpoch, evidenceHash);
}

Three reads are exposed for the rest of the protocol: getReputation(agentId) returns the full struct; scoreOf(agentId) and tierOf(agentId) are the hot paths the CreditManager uses when sizing a line.

A committee, not a key

The oracle is a k-of-n signer committee: a commit needs threshold signatures from distinct, owner-managed signers over the digest above, and submitting the transaction is permissionless — the quorum’s signatures carry the authority, not the sender. So no single key can fabricate a score, and a quorum’s signatures cannot be replayed across chains, deployments, or epochs (all three are baked into the digest). It deploys 1-of-1 for dev and 2-of-3 for the demo, and the engine signs each commit with a quorum of the committee keys. This is what makes “you can’t self-assign reputation” literally true — a user cannot call commit for their own agent. The operator-onboarding flow in Chapter 14 routes through this same gate, which is why the dashboard says the engine attests a starter score.

War story
The checksum that 500’d

The first /onboard call reverted with a 500. The engine derived an agentId from the owner address and packed it for the commit, but a non-checksummed test address tripped viem’s encodePacked address validation. The fix was one line — getAddress(owner.toLowerCase()) to normalize the checksum before encoding. The lesson: addresses crossing the JS/EVM boundary must be checksum-normalized at the boundary, not assumed.

The epoch field is the quiet workhorse. It increments on every recompute, so the engine can detect a stale or replayed commit, and the dashboard can show “epoch N” as a freshness signal. The evidenceHash binds the commit to the exact inputs that produced it — anyone can recompute the score from those inputs and check the hash matches. Reputation that can be re-derived is reputation you do not have to trust.

4
Part II — The On-Chain Spine

CreditVault & CreditManager

What you will learn in this chapter

ERC-4626 is designed for atomic deposit and redemption, not for asynchronous, under-collateralized lending. So the swing keeps two contracts: CreditVault is a clean, composable lender pool; CreditManager sits on top of it and runs the reputation-gated credit logic. Lenders only ever touch the vault; agents only ever touch the manager.

CreditVault — the lender pool

A standard OpenZeppelin ERC-4626 vault over MockUSDC (the demo asset). Lenders deposit and receive scUSDC shares; the share price rises as borrowers repay interest into the idle balance. totalAssets() is idle balance plus totalBorrowed, so interest accrues to lenders implicitly — there is no explicit yield-distribution call.

The classic ERC-4626 inflation/donation attack — where a first depositor donates assets to skew the share price — is mitigated two ways at once: a decimals offset of 6 (virtual shares, so the attacker can never own the entire supply) and a dead-shares seed at deploy (1 USDC deposited to address(0xdead)). The sole borrow authority is the CreditManager:

uint8 constant DECIMALS_OFFSET = 6;     // virtual shares
// at deploy: vault.deposit(1e6, 0xdead)  // dead shares

function borrow(address to, uint256 amount) external onlyCreditManager { ... }
function onRepay(uint256 principal)         external onlyCreditManager { ... }
function writeOff(uint256 principal)        external onlyCreditManager { ... } // on liquidation

CreditManager — the credit logic

The manager reads tier from the oracle, opens a line bound to a specific smart account, and disburses drawn capital only to that account — never to an arbitrary address. The Line struct snapshots the tier and APR at open time and tracks principal, accrued interest, and collateral.

FunctionWhoWhat it does
openLine(agentId, account)permissionlessSnapshot tier & APR, set limit, bind the account
draw(agentId, amount)permissionlessAccrue interest, check borrowing power, disburse to the bound account only
repay(agentId, amount)permissionlessRepay interest first, then principal; tokens flow to the vault
postCollateral / withdrawCollateralpermissionless / ownerManage collateral held by the manager
refreshLimit(agentId)permissionlessRe-read the oracle and re-size the limit when R crosses a tier
liquidate(agentId)onlyOwnerWrite off principal, seize collateral, mark liquidated

The permissionless-everywhere-but-liquidate design is deliberate: it is what makes the user-signed operator-launch flow in Chapter 18 possible. A user can open their own line and draw their own credit directly from their wallet, because the protocol never needed a privileged caller for the happy path.

Borrowing power

fromCollateral   = collateral * 1e4 / collateralFactorBps(tier);
uncollateralized = tier == T3 ? limit : tier == T2 ? limit / 2 : 0;   // T0/T1 → 0
power            = min(fromCollateral + uncollateralized, limit);

So a T3 agent borrows up to its full limit with zero collateral; a T2 agent gets half its limit uncollateralized plus whatever its collateral supports; T0/T1 must post collateral for everything.

Two-slope interest

APR is an Aave/Compound-style utilization curve plus a reputation spread — worse reputation pays more:

BASE_APR_BPS = 200;    // 2% baseline
KINK_BPS     = 8000;   // 80% utilization kink
SLOPE1_BPS   = 400;    // gentle below the kink
SLOPE2_BPS   = 6000;   // steep above it
// tier spread: T0 +2000bps, T1 +1000, T2 +500, T3 +200

Interest accrues lazily — only when draw, repay, or refreshLimit is called — so there is no per-block bookkeeping. Default is expensive in reputation terms: a liquidate emits an event the engine reads to post a negative reputation signal, which is the only enforceable “collateral” in a pseudonymous setting.

5
Part II — The On-Chain Spine

The SpendingGuard and the Rogue-Reject

What you will learn in this chapter

This is where the demo lives. The CreditManager decides how much capital an agent has; the SpendingGuard decides where it may go and how fast. A compromised key that tries to drain the account to an attacker is stopped here — and the rejection is a real, mined, status-0 transaction on the Mantle explorer.

The six-check ladder

All the policy logic lives in one pure library, SpendingGuardLib.sol, ported from the Solana ancestor’s policy.rs. The order of checks is load-bearing — it determines which typed error fires first:

1. Frozen
kill switch on →
Frozen()
2. Destination
payee not allowlisted →
DestinationNotAllowed
3. Per-tx cap
amount > tier cap →
PerTxCapExceeded
4. Daily window
rolling 24h →
DailyLimitExceeded
Figure 5.1 — The ladder. enforce() runs it and, on success, rolls the 24h window and records the spend. preview() mirrors it read-only for off-chain pre-flight, returning a reason code without writing.
struct Config { uint256 perTxCap; uint256 dailyLimit; bool frozen; }  // 0 = unlimited
struct Window { uint64 start; uint256 spent; }

function enforce(Config storage cfg, Window storage win, bool destAllowed,
                 address dest, uint256 amount, uint256 nowTs) external {
    if (cfg.frozen)                          revert Frozen();
    if (!destAllowed)                        revert DestinationNotAllowed(dest);
    if (cfg.perTxCap != 0 && amount > cfg.perTxCap)
                                             revert PerTxCapExceeded(cfg.perTxCap, amount);
    if (nowTs - win.start >= 1 days) { win.start = uint64(nowTs); win.spent = 0; } // roll
    if (cfg.dailyLimit != 0 && win.spent + amount > cfg.dailyLimit)
        revert DailyLimitExceeded(cfg.dailyLimit, win.spent, amount);
    win.spent += amount;   // record on success
}

Two paths, identical behaviour

The same library backs two account implementations, so the rogue-reject behaves identically no matter which one a deployment uses:

Spend metering is shared too, via SpendDecodeLib: it resolves the effective destination and amount of an outgoing call. For native value, that is the call target and msg.value; for an ERC-20 transfer/approve/transferFrom it extracts the token recipient and amount. So “transfer USDC to an attacker” is caught even though the call target is the USDC contract, not the attacker.

War story
Making the revert mine

A reverting transaction normally never reaches the chain: viem runs eth_estimateGas first, which throws, and the client aborts before broadcasting. That would have made the rogue-reject invisible. The fix is to pass an explicit gas limit (gas: 300_000n) on the spend, which skips estimation and lets the transaction mine as status 0 — carrying the guard’s typed error in the receipt. The reverted transaction is the proof. This trick lives in the engine’s spend.ts (Chapter 10).

A GuardedAccountFactory deploys accounts deterministically via CREATE2, with salt keccak256(agentId, owner), so the engine and dashboard can predict() an account’s address before it exists — which is exactly what the operator-launch stepper does to show the user where their agent will live before they sign the create.

The third path: the guard, live on a real Kernel account

The ERC-7579 modules are not a diagram — they run. pnpm --filter @swing/engine aa-demo (and the dashboard’s Spending guard · ERC-7579 panel) spin up a real Kernel v3.1 smart account on Mantle Sepolia, install the SpendingGuardValidator + SpendingGuardHook, and drive spends through a real ERC-4337 bundler with gas sponsored by Pimlico’s paymaster — the owner key never holds a wei. The agent’s session key (the validator) drives the account; every spend it makes is metered by its attached hook. An allowed, in-bounds payment to the allowlisted merchant mines; a rogue one is refused, carrying the same typed error (DestinationNotAllowed, PerTxCapExceeded) the ladder always throws. The engine exposes it over /aa/state, /aa/prepare, /aa/spend off a shared aa.ts core.

There is one honest difference in where the rejection surfaces. The GuardedAccount path mines a status-0 transaction — the revert is on the chain. On the account-abstraction path, a UserOp whose execution would revert is rejected by the bundler at simulation: it never enters the mempool. So the guard makes the malicious operation un-bundleable rather than mined-and-failed — same policy, same SpendingGuardLib, enforced one layer up, surfaced at the bundler instead of in a block.

War story
Five sharp edges in Kernel v3.1

Wiring a custom hook onto a real modular account is a gauntlet of undocumented detail, and the chain gives almost no feedback — Mantle’s public RPC blocks tracing, so a wrong move just succeeds and does nothing. Five things had to be exactly right: (1) the validator-with-hook install data is three ABI segments, not the two a popular SDK emits; (2) Kernel slices a leading flag byte off the hook’s init data, so the policy must be prefixed with 0xff or it decodes to garbage; (3) the UserOp nonce must carry validation-type 0x01 to route to our validator — the SDK’s helper emits 0x00, which Kernel reads as sudo and quietly runs the owner key with no hook at all; (4) the validator must be permitted for the inner execute selector via allowedSelectors, or validation reverts InvalidValidator(); and (5) the callData must be prefixed with the executeUserOp selector so the EntryPoint takes the one path that runs the hook. The bug that cost the most, though, was ours: passing the recipient as the call target, so the account called transfer() on an EOA — a no-op that returns success. Found by decoding the on-chain UserOperationEvent and reading balances, because nothing ever reverted.

6
Part II — The On-Chain Spine

Deployment, Foundry & Verification

What you will learn in this chapter

The spine is seven contracts, deployed to Mantle Sepolia and source-verified so a judge can read the code at sepolia.mantlescan.xyz. Two configuration choices made this work; both are worth knowing because both cost time to discover.

// foundry.toml
[profile.default]
solc          = "0.8.28"
evm_version   = "cancun"   // OZ 5.6 emits MCOPY; Mantle supports Cancun
optimizer     = true
optimizer_runs= 200
bytecode_hash = "none"

[etherscan]
mantle_sepolia = { url = "https://api.etherscan.io/v2/api?chainid=5003", key = "${MANTLESCAN_API_KEY}" }
mantle         = { url = "https://api.etherscan.io/v2/api?chainid=5000", key = "${MANTLESCAN_API_KEY}" }

The evm_version = cancun line matters: OpenZeppelin 5.6 emits the MCOPY opcode, and Mantle’s OP-Stack supports it — but it had to be re-confirmed on Sepolia before the demo deploy. The [etherscan] URLs point at the Etherscan V2 unified multichain endpoint; the old per-host Mantlescan V1 URLs are dead, and discovering that was a half-day detour.

The deploy script

script/Deploy.s.sol deploys all seven contracts and seeds the vault in one run: MockUSDC, ReputationOracle (with the oracle signer), CreditVault (asset = MockUSDC, scUSDC), CreditManager (oracle + vault), GuardedAccountFactory, and the two ERC-7579 module singletons. It then mints 1.1M USDC to the deployer, seeds 1 USDC of dead shares to 0xdead, and deposits 100k USDC as demo liquidity. It writes every address to deployments/5003.json.

Sandbox note

In a sandboxed environment, forge script --broadcast’s fork backend could not hold a stable connection to the public RPC. The reliable path was deploying contract-by-contract with forge create plus retries — single sequential transactions. From a normal terminal, the one-command forge script deploy works fine.

Verification & tests

All eight contracts — the seven spine contracts plus agent #1’s deployed GuardedAccount — are source-verified on Mantlescan using the Etherscan V2 unified endpoint (--verifier-url "https://api.etherscan.io/v2/api?chainid=5003"). The test suite is 60 passing tests across nine files: TierMath.t.sol, ReputationOracle.t.sol, CreditVault.t.sol, CreditStack.t.sol, CreditManager integration, GuardedAccount.t.sol, GuardedAccountFactory.t.sol, and SpendingGuardModules.t.sol — the rogue-tx revert is tested on every rung of the ladder.

7
Part III — The Reputation Engine

The Scoring Core

What you will learn in this chapter

The engine is Node/TypeScript. Its heart is scoring.ts — a pure, deterministic function from an agent’s history to a number in [0,1000]. Pure and deterministic are the whole point: anyone can re-run it and get the same answer, which is what makes the on-chain evidence hash meaningful.

Inputs, normalized

SymbolInputNormalization
zPnlrisk-adjusted realized PnLz-score vs cohort, clipped [-3, 3]
winwin rateλ-weighted mean ∈ [0,1]
oneMinusDd1 − max drawdown[0,1], higher is better
consistencySharpe-like mean/σ of returnssquashed to [0,1]
jobscompleted jobs / validationslog-scaled to [0,1]
validationavg validation response/100
ageaccount age & continuitylog-scaled [0,1]
sybilSybil penalty from passport(15 − passport)/15, [0,1]

The formula

// scoring.ts — λ = 0.5^(ageDays / 30): a 30-day half-life on every event
const raw =
    0.30 * zPnl + 0.15 * win + 0.15 * oneMinusDd + 0.15 * consistency +
    0.08 * jobs + 0.10 * validation + 0.07 * age;

let R = 1000 * sigmoid(1.2 * raw);     // K = 1.2 tunes the spread
R = R * (1 - 0.5 * sybil);             // multiplicative Sybil gate

// hard floor: thin history or weak passport can never exceed T0
if (passportScore < 15 || trades.length < 5) R = Math.min(R, 249);

Every aggregate is a λ-weighted mean, so an event from 30 days ago counts half as much as one today, and one from 60 days ago a quarter. The score therefore reacts immediately to new on-chain activity and lets old losses decay away — a strong recent run can lift an agent into T3, while a long-dormant or thinly-traded account is held at T0 no matter how good its handful of trades look.

The evidence hash

evidenceHash = keccak256(JSON.stringify({
  v: 1, score, weights, k: 1.2, halfLifeDays: 30,
  trades, validations, jobsCompleted, accountAgeDays, passportScore, now,
}));   // committed on-chain alongside the score

This is the link back to Chapter 3. The hash is a canonical fingerprint of every input plus the result. Commit it on-chain and the reputation becomes recomputable: a sceptic feeds the same inputs to the same public function, hashes them, and checks the on-chain commit. Seven vitest cases lock the behaviour — recency halving, T3 promotion for strong agents, the T0 floor for weak passports and thin history, decay of old losses, ranking, and hash determinism.

8
Part III — The Reputation Engine

The Oracle Bridge & Chain Client

What you will learn in this chapter

oracle.ts is the seam between the pure scorer and the chain. commitScore(agentId, inputs) computes the score off-chain, then sends ReputationOracle.commit(agentId, score, evidenceHash) with the engine’s signer and waits for the receipt. commitRaw(agentId, score, evidenceHash) is the same write with a pre-computed score — the path operator-onboarding uses to attest a starter reputation.

The viem client

chain.ts builds the public client, the wallet client (only if a signer key is set), and a set of typed contract instances. One detail is load-bearing:

// chain.ts — Mantle's public RPC rejects default concurrent batches.
export const publicClient = createPublicClient({
  chain: mantleSepoliaTestnet,
  transport: http(RPC_URL, { batch: false, retryCount: 10, retryDelay: 600, timeout: 25_000 }),
});

batch: false forces sequential JSON-RPC calls. Without it, viem coalesces reads into a single batched request and Mantle’s public endpoint intermittently rejects them; with sequential calls and retries, reads are reliable. The registry scanner (Chapter 9) reads ids one at a time for the same reason.

The typed instances are the engine’s read surface: oracle (getReputation, commit), manager (getLine), vault (totalAssets, totalBorrowed, availableLiquidity), usdc (balanceOf, transfer, mint), hook (previewSpend), and a guardedAccountAt(address) factory. Everything downstream — the API, the indexer, the spend executor — reads through these rather than constructing its own calls.

9
Part III — The Reputation Engine

The HTTP API Surface

What you will learn in this chapter

The engine speaks HTTP over Hono on port 8799. Every route is thin: it reads the chain or the scorer, commits when needed, and returns JSON the dashboard renders. The frontend never talks to the chain for reads — it talks to this API.

Method & pathWhat it returns / does
GET /health{ok, chainId, signer, dataSource, explainer, byreal}
GET /deploymentAll deployed contract addresses
GET /agentsLeaderboard (cached registry scan of ids 1–8)
GET /agents/:idReputation, credit line, and account USDC balance
GET /agents/:id/trackTrading record: venue, recent trades, PnL/sharpe/drawdown/equity
POST /agents/:id/recomputeRecompute from current data → commit → explanation
POST /agents/:id/earnSimulate winning trades → recompute → commit (the live demo beat)
GET /eventsRecent on-chain events (ring buffer)
GET /vaultVault stats: assets, borrowed, available, utilization
POST /preview-spendDry-run a spend → reason code + label
POST /spendExecute a real guarded spend (Chapter 10)
GET /x402/:service · POST /x402/:service/buyThe HTTP-402 round-trip (Chapter 11)
GET /byreal/marketLive Byreal CLMM context (Chapter 13)
GET /onchain · POST /onchain/commit/:idVerified Mantle-mainnet PnL record & commit (Chapter 13)
POST /onboardAttest a starter reputation for a wallet’s derived agent (Chapter 14)

The /earn endpoint is the demo’s pulse: it appends a handful of winning trades to the agent’s history, recomputes the score, commits it to the oracle as a real transaction, and returns the before/after scores plus a GLM explanation. The dashboard’s “Run a trading session” button is a single call to it.

Background tasks

Two loops keep reads instant. startIndexer() scans the oracle, manager, and hook for new events every 12 seconds (backfilling 2,000 blocks on first run) and pushes them into a 200-entry ring buffer, so /events is an in-memory read. startRegistry() sequentially scans agent ids 1–8 every 20 seconds and caches those with a score or an open line, so the /agents leaderboard never blocks on the chain.

10
Part III — The Reputation Engine

Live Spend & the On-Chain Rogue-Reject

What you will learn in this chapter

The signature beat from Chapter 5, made live. spend.ts exposes executeSpend({accountAddr, to, amount, ensureFunds}), which submits a real GuardedAccount.execute() to Mantle Sepolia and returns the mined outcome.

// spend.ts — the explicit gas limit is the whole trick
const hash = await walletClient.writeContract({
  ...guardedAccountAt(accountAddr),
  functionName: "execute",
  args: [to, value, data],
  gas: 300_000n,                 // skip eth_estimateGas, which would throw on a guarded revert
});
const rcpt = await publicClient.waitForTransactionReceipt({ hash });
return { txHash: hash, status: rcpt.status, to, amount, reason };  // status: "success" | "reverted"

Without the explicit gas, viem estimates gas first, the guard’s revert throws, and the transaction never broadcasts — the rejection would be a client-side exception, not an on-chain fact. With it, the transaction mines as status 0, carrying the guard’s typed error in the receipt. An allowlisted, in-bounds spend mines as status 1. previewSpend returns the reason code first so the UI can label the verdict, and ensureFunds mints test USDC into the account when it is low so the demo never stalls on a dry balance.

Why a revert beats an error message

A judge can open the reverted transaction on Mantlescan and see status 0, Destination not allowlisted, and an attacker balance of zero. The funds never moved, and the proof is a public, immutable receipt — not a screenshot of an app saying “blocked”. This is the difference the whole project is built to demonstrate.

11
Part III — The Reputation Engine

x402 Agent-to-Agent Payments

What you will learn in this chapter

Agents do not only borrow — they earn. x402.ts implements a faithful HTTP-402 flow so one agent can buy a service (here, agent #1’s momentum signal) from another, with real settlement on Mantle. The ERC-8004 spec explicitly points at x402 for payment-enriched feedback; the swing closes that loop.

GET service
no payment
402
+ accepts
(price, payTo)
settle()
USDC transfer
on Mantle
200
+ X-PAYMENT-RESPONSE
+ signal
reputation
proof-of-payment
→ commit
Figure 11.1 — The x402 round-trip. A GET without payment returns 402 and the requirements; the buyer settles on-chain, retries with an X-PAYMENT header, and the server verifies the Transfer log before delivering the signal.

The verifier is strict. It decodes the base64 X-PAYMENT token to a tx hash, fetches the receipt, and confirms a Transfer log to payTo of at least the required amount. Replays are blocked by a consumed set keyed on the lowercased tx hash — a payment can redeem a service exactly once. On success, the engine calls recordPaidJob on the provider, recomputes its score, commits it, and returns an X-PAYMENT-RESPONSE header. The POST /x402/:service/buy endpoint runs the whole round-trip autonomously for the dashboard, returning the quote, the settlement tx, the delivered payload, and the reputation delta.

12
Part III — The Reputation Engine

Z.ai GLM Explanations

What you will learn in this chapter

Z.ai is both a sponsor and a judge, so using GLM as the reasoning layer is strategically smart and technically sound. explain.ts narrates each reputation/credit move — “Reputation 500 → 750/1000 (T2 Established). Promoted. Drivers: win-rate 75%, drawdown-health 95%, consistency 88%, validation 94%. Sybil penalty 10%.”

// glm.ts — fast, and never blocks a request
const res = await fetch(`${ZAI_BASE_URL}/chat/completions`, {
  method: "POST",
  headers: { Authorization: `Bearer ${ZAI_API_KEY}`, "content-type": "application/json" },
  body: JSON.stringify({
    model: ZAI_MODEL,                       // glm-4.5-flash (free tier)
    temperature: 0.4, max_tokens: 256,
    thinking: { type: "disabled" },         // reasoning off → ~3s replies, not 15s
    messages: [{ role: "system", content: SYSTEM_PROMPT }, { role: "user", content: factSheet }],
  }),
  signal: AbortSignal.timeout(15_000),      // a slow provider never stalls the request
});

Two settings are load-bearing. thinking: { type: "disabled" } turns off the reasoning-model chain so replies come back in about three seconds instead of timing out; the free glm-4.5-flash tier is used because glm-4.6 needs a paid balance. The 15-second abort means a slow or unreachable provider degrades gracefully — explain() falls back to a deterministic template and logs the fallback, and /health reports explainer: glm only when the key is live.

War story
The number that didn’t match

An early fact sheet handed GLM a credit-limit figure, and the model cited “$37,945” in its prose while the credit panel rendered “$35,777” from a live contract read. Two sources of truth for one number is one too many. The fix: the fact sheet now carries only score, tier, stance, and drivers — no dollar figures. Dollar amounts come exclusively from on-chain contract reads, so the narration can never contradict the chain.

13
Part III — The Reputation Engine

Byreal Context & the On-Chain PnL Indexer

What you will learn in this chapter

Byreal market context

byreal.ts shells out to the official @byreal-io/byreal-cli for real overview data (TVL, 24h volume, fees, pool count), caches it for 60 seconds, and degrades gracefully if the CLI is absent. The framing is honest: Byreal’s CLMM is on Solana, so this is live signal context the agent reads — reputation, credit, and safety stay on Mantle. The dashboard labels it as such.

The verified on-chain PnL record

The honest answer to “is this real on-chain activity?” lives in data/mantleOnchain.ts. It reads a real trader’s ERC-20 transfers on Mantle mainnet (chain 5000) via the Etherscan V2 account API, groups them by transaction into swaps, and reconstructs realized PnL in USD — using stablecoin legs as the numéraire, with no price oracle.

// data/mantleOnchain.ts — stablecoins are the numéraire
const STABLES = new Set(["USDC","USDT","USDT0","USDe","USDY","mUSD","axlUSDC"]);
// per clean 1-in / 1-out swap:
//   stable OUT, token IN  → BUY:  add to position (qty, avg cost)
//   stable IN,  token OUT → SELL: realize PnL = stableIn − avgCost*qty, close position
// multi-hop / LP transfers are skipped for clean reconstruction.

The result is a full record: realized PnL, win rate, closed trades, open positions, a span in days, and every trade as a clickable mainnet transaction. Those reconstructed trades become ScoreInputs the same scorer consumes, so the engine produces a real, derived score (the discovered trader at 0x4993…0814 scores 710, T2 — 84% win, +$768 realized). The POST /onchain/commit/:id route commits that derived score to the Sepolia oracle for an isolated agent slot — live swaps → realized PnL → reputation → on-chain commit, end to end, with nothing simulated.

Discovered, not hand-picked

The benchmark wallet is found on-chain, not curated. A helper (pnpm --filter @swing/engine scan --router <dex-router>) pulls recent traders from a DEX router and ranks them by the engine’s own reputation score — so the wallet on screen is genuinely one the engine rates highly (here, 710/T2), not a high-volume break-even bot. It stays a labelled reference benchmark on the /agents page, not the holdings of the selected agent, because clean PnL reconstruction needs stablecoin-legged round-trips, which most addresses lack. Point it at a RealClaw agent by dropping its address into MANTLE_TRADER_ADDRESS.

14
Part III — The Reputation Engine

Operator Onboarding

What you will learn in this chapter

POST /onboard is the bridge between “a person with a wallet” and “an agent with credit.” The engine attests a starter reputation for the wallet’s derived agent — the user cannot do this themselves, because only the engine signer may write the oracle (Chapter 3).

// api.ts — deterministic, collision-free for 900k addresses
const owner   = getAddress(body.owner.toLowerCase());          // checksum-normalize at the boundary
const agentId = 100_000n + (BigInt(owner) % 900_000n);         // derive a 6-digit id from the wallet
await commitRaw(agentId, 700, evidenceHashFor(owner));         // starter score 700 → tier T2
return { agentId, owner, score: 700, tier: 2, tierName: "Established", evidenceHash, txHash };

The starter score of 700 lands the new agent in T2 — enough to unlock a meaningful, partially-uncollateralized line so the demo has something to draw against. The id is derived from the owner address (100_000 + owner % 900_000), so it is deterministic and unique per wallet, but it sits far outside the 1–8 range the registry scanner enumerates — which is why the frontend remembers launched agents in local storage (Chapter 18).

This one endpoint is what turns the operator-launch stepper into a genuinely user-owned flow: the engine attests reputation (oracle-signed), and then the user’s own wallet signs everything else — create account, open line, draw. Reputation → credit → spend, owned end-to-end by the person at the keyboard.

15
Part IV — The Frontend

The Design System

What you will learn in this chapter

The dashboard is Next.js 15 with Tailwind v4, and its look is deliberate: a near-black, zero-saturation palette where the only colour is red, reserved exclusively for danger — the rogue, the blocked, the reverted. Every other meaning is carried by brightness. A judge should feel, before reading a word, that this is a serious instrument, not a toy.

The palette

Colours are defined as oklch CSS variables in globals.css. The achromatic ramp runs from paper (near-black background) to ink (near-white text); red is the single chromatic exception.

TokenValueRole
--color-paperoklch(0.145 0 0)page background
--color-cardoklch(0.178 0 0)panel background
--color-inkoklch(0.985 0 0)primary text (near-white)
--color-muted / --color-faintoklch(0.78 / 0.6)secondary / tertiary text
--color-line / line-strongoklch(0.27 / 0.37)hairline borders / focus
--color-dangeroklch(0.71 0.19 23)the only hue — rogue / blocked
--color-t0 … t3oklch 0.6 → 0.985trust tiers, dim → pure white

The tier ramp is the cleverest part. A reputation score is encoded as brightness: T0 is a dim grey, T3 is pure white. A glance at a leaderboard reads as a gradient of trust without a single coloured pixel — which keeps red meaningful. When the eye finds red, something is wrong, every time.

Type, cards, and stars

Typography is Geist Sans for display and body, Geist Mono for addresses, hashes, and amounts (with tabular-nums so trailing zeros line up). Cards share a 12px radius, a 1px line border, and a subtle inset highlight over a deep outer shadow — a quiet, instrument-panel feel. Behind everything, StarField.tsx paints 120 twinkling, slowly-falling stars onto a canvas at z-index −10, ported from the Entrypoint Labs site — cosmic, distant-future, never distracting.

Production, not dev

The dashboard is served with next start (production), reading the engine on :8799. That means every UI change needs a pnpm build and a restart to appear — a deliberate choice for a stable demo surface, and a thing to remember when iterating.

16
Part IV — The Frontend

The Pages

What you will learn in this chapter

Five routes, each a distinct job in the story: convince, operate, rank, lend, launch.

RoutePurpose
/ landingThe thesis in one screen: hero line (“Agents earn trust. Capital follows. Rogue spends revert.”), live stats from /vault and /agents, a five-step “how it works”, and the five-piece architecture overview.
/console hubThe operational dashboard for one agent — reputation gauge, credit panel, trading record, activity feed, x402, and the spending-control simulator. Polls the engine every 7s.
/agents registryA brightness-ranked leaderboard of scored agents, the connected wallet’s “Your agents” section, and the labelled real-world on-chain benchmark trader. Refreshes every 8s.
/vault lendThe ERC-4626 pool: TVL, utilization, and the user-signed lender flow (faucet → approve → deposit → withdraw).
/launch operatorThe four-step stepper that turns a wallet into a credit-backed agent — engine attests, then the wallet signs create → open line → draw.

The landing page is unapologetically a pitch: it leads with the rogue-reject and the earn→credit loop in the first two beats, exactly as the rubric rewards. The console is where a judge watches the loop happen for a single agent; /vault and /launch are where they can participate with their own wallet rather than watch a server-signed puppet.

17
Part IV — The Frontend

The Components

What you will learn in this chapter

Console.tsx is the composition. In order, it stacks: the agent header and reputation gauge, the credit panel, a live Byreal market strip, the trading record, the activity feed, the x402 panel, and the spending-control simulator. It fetches agent state, track, and events every 7 seconds; the “Run a trading session” button fires api.earn(), commits on-chain, and animates the score moving.

Reputation & credit

Record, activity, and live integrations

Two more carry the brand: StarField.tsx (the canvas backdrop) and ui.tsx (the shared primitives — Card, Label, Dot, ExplorerLink, Copyable). tiers.ts centralizes the tier styling (TIER_STYLE and tierHex) so brightness is defined once. The discipline across all of them is the same: no mocks — every verdict on screen is a mined Mantle Sepolia transaction with a status code and a link.

18
Part IV — The Frontend

Wallet-Connected Product Flows

What you will learn in this chapter

The guided demo proves the loop without the viewer owning anything. These flows let a judge use their own wallet — lend real test capital, or launch their own credit-backed agent and sign every step.

The wallet stack

lib/wagmi.ts is a single-chain wagmi v2 config: mantleSepoliaTestnet, the injected() connector (MetaMask / Rabby / Brave), and an HTTP transport to rpc.sepolia.mantle.xyz. Providers.tsx wraps the app in WagmiProvider + QueryClientProvider. (wagmi v3 broke on a barrel-import bug, so the stack is pinned to the stable v2 line.)

Network handling

Network.tsx centralizes the “are we on Mantle?” question. useMantleGuard() exposes { wrongNetwork, switchToMantle }; useAutoSwitchMantle() — mounted once in the header — auto-prompts the switch (and the add-chain) the moment a connected wallet is on the wrong chain. WrongNetworkBanner renders above any signing area, and every signing button (Draw, Deposit, Create…) is disabled while off-chain, so a transaction can never fire on the wrong network and throw a cryptic error.

The lender flow

LenderPanel.tsx on /vault: connect → faucet-mint test USDC → approve → deposit → hold scUSDCwithdraw. Standard ERC-4626, fully user-signed, reading balances through useReadContracts against the minimal ABIs in lib/contracts.ts.

The operator-launch stepper

OperatorLaunch.tsx on /launch closes the whole loop with the user’s wallet:

1. Score
engine attests
(oracle-signed)
2. Create account
wallet signs
factory.createAccount
3. Open line
wallet signs
manager.openLine
4. Draw
wallet signs
manager.draw
Figure 18.1 — Step 1 calls /onboard (you can’t self-assign reputation); steps 2–4 are signed by the user’s own wallet. The stepper reads the predicted account via factory.predict and borrowing power via manager.borrowingPower before each signature. Verified on-chain end to end via cast: $5,000 borrowing power at T2, $500 landed in the account.

Because the launched agent’s id is derived (100_000 + owner % 900_000) and sits outside the registry’s 1–8 scan, MyAgents.tsx remembers it: lib/myAgents.ts stores the id in localStorage keyed by wallet, and the component reads each agent’s live state back from chain by id — so a refresh never loses an agent you launched. It appears on both /launch and /agents, linking through to its console.

One curve, two languages — again

The frontend never re-implements protocol math. It calls factory.predict for the account address, manager.borrowingPower for the limit, and the shared tier-math mirror for display — so what the UI shows before a signature is exactly what the contract enforces after it. Honest by construction.

A
Part V — Reference

The File Map

A monorepo in three workspaces: the contracts (the truth), the engine (the projection), and the web app (the surface), with a shared package binding them.

contracts/                     # Foundry — the spine, the source of truth
  src/
    ReputationOracle.sol         R + tier per agentId, engine-signer-gated
    CreditVault.sol              ERC-4626 lender pool + inflation mitigation
    CreditManager.sol            tier → limit, open/draw/repay/liquidate, 2-slope interest
    libraries/
      TierMath.sol               pure: tier(R), limit(R), caps, collateral factors
      SpendingGuardLib.sol       the six-check ladder, shared by both account paths
      SpendDecodeLib.sol         resolves effective dest + amount of a call
    accounts/
      GuardedAccount.sol         minimal direct wallet (bundler-independent)
      GuardedAccountFactory.sol  CREATE2 deploy + predict()
    modules/
      SpendingGuardValidator.sol ERC-7579 Type 1 — scoped agent session key
      SpendingGuardHook.sol      ERC-7579 Type 4 — runs the ladder in preCheck
  script/Deploy.s.sol            deploys all 7 + seeds the vault
  deployments/5003.json          canonical Mantle Sepolia addresses

packages/shared/               # TS mirror: tier math, ABIs, config, types

apps/engine/src/               # Node/TS reputation engine (Hono + viem)
  scoring.ts                     the formula + evidence hash
  oracle.ts / chain.ts           commit bridge + viem client (batch:false)
  api.ts                         the HTTP surface (Chapter 9)
  spend.ts                       live guarded spend (explicit gas → status 0)
  x402.ts / glm.ts / explain.ts  payments + GLM narration
  byreal.ts                      live Byreal CLMM context (Solana, read-only)
  data/mantleOnchain.ts          real Mantle PnL reconstruction
  registry.ts / indexer.ts       leaderboard scan + event ring buffer

apps/web/                      # Next.js 15 dashboard
  app/  (page.tsx, console, agents, vault, launch)
  components/  (Console, ReputationGauge, CreditPanel, TrackRecord,
                SpendingControls, X402Panel, OnchainPanel, LenderPanel,
                OperatorLaunch, MyAgents, Network, ConnectButton, StarField, ui)
  lib/  (api, config, wagmi, contracts, format, myAgents)
B
Part V — Reference

Deployed Addresses & Environment

Live on Mantle Sepolia (chainId 5003). Deployer / oracle signer: 0x4D6A7d6bF3C0a885D581AacEC0345526bd33273E. Source-verified on Mantlescan.

ContractAddress
MockUSDC0xa29799A188C220B17788a355Ec0166523172B09d
ReputationOracle0xe00962601106D055be7A1f97CD53c9C7B4b46632
CreditVault (ERC-4626)0x0aD20c99D72AA4371317a85A85Ce39C318a2b114
CreditManager0x1be497f127561a8F3e53aF53452Ce6cdC09e31a8
GuardedAccountFactory0xB1ccd35E453eB0a4eeD05a3AE0BFC638B397B997
SpendingGuardHook (7579 T4)0x9A0735F793e438b63241252EB54ef7B519E698Bb
SpendingGuardValidator (7579 T1)0x60C3C40566a932bAcA3AfD23699C38e9F0F3E2C3
Demo GuardedAccount (agent 1)0xA6f857F91C57f6DaC7BAf5F4A2abfA026A729365

The verified loop, on-chain (agent 1)

StepResult
Reputation commitR = 832 → tier T3
Credit lineborrowing power 37,945 USDC = creditLimit(832)
Draw5,000 USDC disbursed to the guarded account
Allowed spend1,000 USDC → allowlisted merchant  ✓ status 1
Rogue spend→ non-allowlisted attacker  ✗ REVERTED, status 0

Key environment variables

The real .env (private key) stays gitignored; only placeholders live in .env.example. Run the stack with ENGINE_PORT=8799 pnpm --filter @swing/engine start + pnpm --filter @swing/web start (web on :3000 reads engine on :8799).

C
Part V — Reference

The 90-Second Demo Run-of-Show

The whole loop, told in ninety seconds. The climax is beats 6–7: the drain that reverts on-chain.

#On screenBeat
0Landing“Agents earn trust, capital follows, rogue spends revert. The whole loop on Mantle.”
1Console, agent #1ERC-8004 identity + a track record; the engine scores it and commits on-chain.
2Run a trading sessionIt trades; the engine recomputes and commits the update — a real oracle tx.
3Credit panelScore → tier → an uncollateralized limit funded by the ERC-4626 vault.
4x402 panelAnother agent buys this one’s signal over x402; proof-of-payment loops back as reputation.
5Spending controls“Pay merchant” — an allowlisted, in-bounds spend executes (status 1).
6Spending controls“Drain to attacker” — the card shakes red.
7MantlescanClick the tx → status 0 (failed), the guard’s typed error, funds never moved.
8Console“Identity, reputation, credit, payments, safety — every step a verifiable Mantle event.”

Beyond the guided demo, the presenter can connect their own wallet and either lend on /vault or launch their own credit-backed agent on /launch — turning the pitch from “watch this work” into “do it yourself.”

D
Part V — Reference

Glossary

TermDefinition
ERC-8004The “trustless agents” standard: on-chain registries for agent identity, reputation, and validation. The swing keys reputation by ERC-8004 agentId.
ERC-4626The tokenized-vault standard. CreditVault is one; lenders deposit and earn yield as borrowers repay interest.
ERC-7579Modular smart-account standard with typed modules. The swing ships a Type-1 validator and a Type-4 hook for the guard.
x402The HTTP-402 agent payment rail: a server returns 402 with terms, the client pays on-chain and retries with proof.
Tier (T0–T3)Trust bands derived from reputation R, setting credit limit, caps, and collateral. T0 unproven → T3 trusted.
Evidence hashA keccak fingerprint of the exact scoring inputs, committed on-chain so a score can be independently recomputed.
Rogue-rejectThe signature beat: a policy-breaching spend that mines as a status-0 reverted transaction on Mantle.
The numeraireStablecoins (USDC/USDT/…) used as the unit of account to reconstruct realized PnL from DEX swaps without a price oracle.
GuardedAccountThe minimal, bundler-independent smart wallet that runs the spending guard directly — the guaranteed demo path.
scUSDCThe ERC-4626 share token a lender receives for depositing USDC into the CreditVault.

earn → reputation → credit → deploy → rogue-reject — every step a verifiable Mantle event. That is the swing.