Before you run this: what can actually move, and what cannot
Settlement runs on three testnets: an EVM escrow deployed on both
BNB Chain (chainId 97) and Base Sepolia (chainId 84532) settling test USDC, and a
Solana devnet Anchor program (program id
4EDeeLJNbh9AXdLsfkgHswJ9VS2RX7yad9wV9pU7Ebgs) settling devnet SPL tokens — all
funds with no real-world value. The same canonical verdict settles on any rail.
Treat everything here as a demonstration of a mechanism, not a production payments rail.
The deterministic grader itself is open and runnable today: npm run grade needs
no keys and no account — keys only enter when you sign a verdict and settle.
The mechanism is real, and you should understand it before pointing it at anything that matters.
When Grain settles an order, an autonomous on-chain transaction either releases USDC to
the seller (a pass verdict) or refunds the buyer (a fail verdict).
In a production deployment on mainnet that would move real value, autonomously, the moment a
signed verdict lands. Settlement is one-shot and irreversible — once an order
leaves the Funded state it cannot be re-settled or rolled back.
Two rules make this safe to operate, and Grain enforces both in code rather than asking you to trust it:
- Never fabricate. The grader signs a verdict only over the actual sha256 of the GLB it judged and the real predicate results. It never invents a pass, a balance, or a transaction hash. If you reproduce a settlement, read the state on-chain — never assert a value you have not verified.
- Confirm before you go live. Funding an order (
fundOrder) is the explicit human/upstream gate: it pulls USDC from the buyer and commits the buyer, seller, and grader write-once. Submitting the verdict afterward is intentionally autonomous (the signed verdict is the gate), so the place to stop and confirm is before you fund a live order, not after.
Verdict authority and custody are different powers held by different code paths. The grader's key can do exactly one thing — sign a binary pass/fail over a pre-funded order. Funds always move to the buyer or seller recorded at funding time; never to the signer, never to whoever submits the transaction, never to an address supplied at settlement. So even a fully compromised grader key cannot exfiltrate a single cent — the worst it can do is mis-grade between two parties you already agreed on. There is no owner, admin, pause, upgrade, setter, or withdraw function: a test asserts the contract's entire public surface is just five functions. The same property holds on both rails — verdict authority and custody are separated in code on the EVM escrow (EIP-191) and on the Solana program (ed25519) alike; adding the second rail was a new adapter, not a re-architecture of who can move money.
See it in action: grading a bad crate, then a good crate
On a fresh clone, npm run grade grades the bundled hero mesh against the public
sample-game-prop@1 profile and prints a real per-predicate verdict
— no account, no API keys, no chain. Keys are only needed to sign a
verdict and to settle it on-chain. Bring your own bar instead of ours: pass any
acceptance profile inline via the CLI (npm run grade asset.glb --profile my.json)
or the HTTP API (POST /grade with an inline profile body — no
registration). The private tuned game-ready-prop@1 thresholds (the moat) are never
required to run the grader; they stay gitignored.
Call grade_asset with { assetUri, profileId: "sample-game-prop@1" }
(assetUri is a URL or path to the GLB; this is the same public profile
npm run grade uses, so a reader can reproduce every row below).
You get back { pass, results[], predicateDigest, assetHash, verdict }, where
results is one row per predicate (each { id, label, pass, actual, expected,
evidence }), run in fixed id-sorted order. The profile sample-game-prop@1 runs
in mode all — every predicate must pass. Its deterministic floor is six predicates:
valid_glb, tri_count, uv_present, texture_size,
file_size, and manifold. (The private
game-ready-prop@1 tightens these — watertight required, ≤ 10k tris — and adds a
required named_params param; same engine, stricter bar.)
Round 1 — a bad crate (over budget, open seams, oversized texture, no UVs)
A raw fal Rodin export: over the triangle budget, with seams the exporter left open, an unwrapped
mesh with no UVs, and a texture above the budget.
pass: false → the signed verdict
is fail → settlement refunds the buyer.
| id | label | pass | actual* | expected |
|---|---|---|---|---|
file_size | File-size budget | ✓ true | 6.1 MB | ≤ 8 MB |
manifold | Watertight | ✗ false | 312 boundary edges | 0 non-manifold edges |
texture_size | Texture budget | ✗ false | 8192 px | ≤ 4096 px |
tri_count | Triangle budget | ✗ false | 41,980 | ≤ 50,000 |
uv_present | UVs present | ✗ false | no UV channel | UV0 required |
valid_glb | Valid GLB | ✓ true | 0 errors | ≤ 0 errors |
Round 2 — a good crate (decimated to budget, welded, unwrapped, texture resized)
After Grain decimates to budget, welds the mesh, unwraps clean UVs, and resizes the texture into
budget, every row flips to pass.
pass: true → the signed verdict
is pass → settlement releases payment to the seller.
| id | label | pass | actual* | expected |
|---|---|---|---|---|
file_size | File-size budget | ✓ true | 2.4 MB | ≤ 8 MB |
manifold | Watertight | ✓ true | 0 non-manifold edges | 0 non-manifold edges |
texture_size | Texture budget | ✓ true | 2048 px | ≤ 4096 px |
tri_count | Triangle budget | ✓ true | 9,874 | ≤ 50,000 |
uv_present | UVs present | ✓ true | UV0 present | UV0 required |
valid_glb | Valid GLB | ✓ true | 0 errors | ≤ 0 errors |
* id, label and expected
are exact; actual values are illustrative of the real evidence shape. Rows render in
id-sorted order — that ordering is what makes predicateDigest reproducible.
The stricter private game-ready-prop@1 additionally requires a watertight mesh,
≤ 10k tris, and a declared named_params param (accent_color).
Optional: the semantic row
If you also pass intent: "a game-ready wooden crate prop", an opt-in fifth predicate
semantic_match renders the asset to fixed views, CLIP-scores it against the intent
plus distractors, and is folded in as a hard gate — it passes only if the intent
is the top match at or above the threshold (default 0.6). It is deterministic on a
single pinned grader (cross-grader agreement requires pinning the render + CLIP
model versions); the six-predicate deterministic floor stays the disputeless core.
| id | label | pass | actual* | expected |
|---|---|---|---|---|
semantic_match | Matches intent | ✓ true | topMatch: "a game-ready wooden crate prop", intentScore: 0.91 | "a game-ready wooden crate prop" is the top match @ ≥ 0.6 |
The calibration behind that threshold (default
0.6; max-F1 T ≈ 0.5–0.6) comes from honest-v0, an
N=5 benchmark of visually-distinct fal Rodin assets (crate, barrel, vase, helmet,
chair) — it proves the method and the deterministic floor, not the hard/confusable
cases; a larger, harder set is roadmap.
When an escrowId was supplied, the result also carries a canonical
verdict — the same tuple drives either rail:
verdict: { escrowId, result: "pass", assetHash, graderSignature } (plus the chain
binding it was signed for — escrowAddress/chainId on EVM, or
programId/cluster on Solana; full addresses in §06).
Both adapters consume the same canonical verdict: the EVM escrow recovers the
EIP-191 signature and requires it to equal the order's grader address; the Solana program
verifies an ed25519 signature against the grader pubkey. One signed verdict, two rails.
The verdict is decided by deterministic code (and the opt-in CLIP check), never by an LLM
guessing; the Grader Mind's Brain only narrates a verdict it did not decide.
Before you start
Grain orchestrates three Minds in one Circle (Requester → Generator → Grader). The platform has no Builder API to set a Mind's DNA, Tenets, or Skills, so setup is part console, part conversation. Everything below runs on testnet — no real money moves.
- A Minds (by Animoca) account. Builder beta at build.hellominds.ai; sign-in is email-based.
- Awaken each Mind via the Concierge. There is no "create Mind" API and no UI form for DNA/Tenets — you "Launch a Mind," the Concierge emails you, and you describe the role in natural language. Awaken three: a Requester, a Generator, and a Grader. Set the role DNA at creation — role-locked Minds work reliably, whereas re-tasking a default "companion" Mind by chat is inconsistent (it tends to defend its identity and refuse).
- Durably seed each role. The sanctioned path is conversational: message the Mind to invoke its own internal
TENET_Updatetool, framing the Grain role as an added operating tenet (not an identity replacement). Identity-level reframing ("you ARE the Grader") trips a guardrail and is refused. - Equip the Grain Skill. The deterministic grader publishes as a Bazaar Skill (a multi-step playbook over Tools), distinct from the Mind's DNA. Build it by describing the outcome to the Mind in chat; edit it later via
REGISTRY_Update/SKILL_Update. (Publish is in progress in this build.) - Add Connections for credentials. Secrets — the grader signing key, any API keys — live platform-side in Connections. The Mind never holds the key, and the private acceptance-profile thresholds (the moat) stay out of chat and out of the public repo.
- Put all three Minds in ONE Circle. Circles are UI-only — there is no Circle-management API.
- Fund cognition on every Mind. Each reply spends cognition; a 0-balance Mind silently times out. Known bug: topping up a brand-new Mind immediately after creation can charge without minting credits — fund after the wallet registers, and hard-refresh (the balance isn't real-time).
- A funded buyer key. Holds Base Sepolia test USDC to call
fundOrderand escrow an order againstGrainEscrow(0xEEb15426d656dCfb76AC058D1A1e66c821cB78d9). - A gas-paying keeper key. Holds Base Sepolia ETH to submit
settleOnAttestation; it has zero fund authority. The Mind's own Base wallet (an EOA) cannot broadcast settlement. - A chosen acceptance profile. Two ship in the box, and they are not the same thing. The runnable public sample is
sample-game-prop@1(valid GLB, ≤ 50k triangles, UVs present, texture ≤ 4096px, file ≤ 8 MB, non-manifold-free) — you can run and edit it with no keys vianpm run grade. The private tunedgame-ready-prop@1is the moat (watertight required, ≤ 10k triangles, a required named paramaccent_color, plus an optionalsemantic_matchcheck) — it stays gitignored. The public predicate code is worthless without the private profile thresholds. You can also bring your own bar: pass an inline acceptance profile via the CLI--profileflag or the HTTP/gradeprofilebody — no registration, no keys to grade.
What Minds can do (and what they can't)
A Mind = Soul + Brain. The Soul holds durable DNA (permanent values, set at awakening), Tenets (editable rules; invariant ones are Guardrails), Memory, State, and a Wallet. The Brain is reasoning, auto-routed across hosted vision-capable models and separate from the Soul — so a Mind keeps its role even when the underlying model changes. Grain adds one thing the platform doesn't have: a deterministic judge.
General Minds capabilities
- Persistent identity + memory — proven live: the Grader Mind, seeded once via
TENET_Update, restated its role and acceptance criteria on a later, separate call. - Natural-language interface — you drive a Mind by messaging it; in Grain this carries the legible "why it was rejected" narration (flavor on top of the deterministic verdict, never the verdict itself).
- Equippable Skills — behavior is a layer separate from DNA: multi-step playbooks over Tools, published to the Bazaar, equippable by other Minds, editable conversationally after publish.
- Autonomous, background operation on a cognition budget — Grain leans on this: settlement releases/refunds on the signed verdict with no human approval.
- Email, Telegram, and Circles — Mind-to-Mind messaging genuinely works (a Generator Mind delivered a unique token to the Grader in a shared Circle, confirmed on the receiver side).
What your Mind can do with Grain
- Grade a delivered GLB against a named profile by calling
grade_asset, and get a per-predicate report. The deterministic floor is seven predicates —tri_count,manifold,valid_glb,uv_present,texture_size,file_size,named_params— each with the measuredactual, theexpectedthreshold, andevidence(any given profile selects which of these fire; the publicsample-game-prop@1runs six, the privategame-ready-prop@1addsnamed_params). - Re-grade the same GLB + profile anytime and get a byte-identical
predicateDigest— reproducible and timestamp-free. - Optionally add the opt-in
semantic_matchcheck by supplying anintent, folding "is this actually the right thing?" into the verdict as a hard gate. - Sign one canonical verdict bound to an order and settle it on either rail: an EIP-191 signature that
GrainEscrow.settleOnAttestationrecovers to the grader address on Base Sepolia or BNB Chain, or an ed25519 signature the Solana devnet program verifies against the grader pubkey — release (pass) or refund (fail) without trusting the counterparty, because the sign-only key can never pay itself on either chain. - What it does NOT yet judge: LODs, full PBR/material budgets, fidelity-to-reference, provenance/IP, parametric editability, and aesthetic taste — these are roadmap, not built. (UV presence and a texture-size budget are shipped now —
uv_presentandtexture_size— so those came off this list.) The same rails take a new predicate set per vertical, but only 3D is proven today.
- No Builder API to set DNA / Tenets / Skills — provisioning is Concierge-at-creation or conversational
TENET_Updateonly. - Mind↔Mind traffic is invisible to the Builder API and there's no Circle-management API — which is exactly why Grain ships an observable hybrid controller rather than relying on minds-native messaging.
- The Mind's on-chain wallet can't settle — it's an EOA with no broadcast path; an external keeper submits the transaction.
- Cognition balance (the Mind's thinking-spend) isn't real-time or API-readable — you can't poll it to auto-throttle a Mind's own compute. This is separate from the order budget: the USDC a buyer escrows is set at
fundOrderand fully readable on-chain, so Grain's spend guardrails live there (much as Superior reads trading capital from its own API — not from Minds cognition). - Conversational role-seeding is unreliable on default "companion" Minds — reliable roles must be set as DNA at awakening.
Step-by-step guide
Six moves mirroring the connect → prompt → preview → approve → execute → monitor pattern, adapted to Grain's request → generate → grade → settle loop.
Grain takes no live settlement without an explicit signed gate — here the gate is the grader's EIP-191 verdict, not a human click — but the Mind still surfaces the full evidence before anything settles, and the human gate sits upstream at funding.
Equip the Grain Skill
grade_asset / generate_asset / settle tools are available; calls the hosted grader through the Connection and never holds the key.State the request, budget, and profile
sample-game-prop@1." (Want a tighter bar? name the private game-ready-prop@1, or hand the Mind your own inline profile.)profileId selects which deterministic predicates fire and their thresholds; a BYO inline profile works too, with no registration.sample-game-prop@1 is open by design.Generate a candidate
generate_asset produces a GLB (commodity image/text→3D, e.g. a fal Rodin path) and, for a profile that requires it, attaches the declared editable params so named_params is meaningful. Generation is generation-agnostic — Grain grades whatever bytes arrive.assetHash the verdict commits to — are deterministic (params sorted).Grade (signed verdict)
grade_asset parses once, runs the predicates in sorted order (+ semantic_match if an intent is given), composes pass/fail (AND, or weighted per profile), computes predicateDigest, and signs the verdict when an escrowId is supplied.{ pass, results[], predicateDigest, assetHash, verdict }. No escrowId → verdict: null (grade-only).Review the pass/fail + evidence
Confirm settlement (gated), then monitor
settle submits the canonical verdict to the chosen rail — settleOnAttestation(escrowId, result, assetHash, graderSignature) on the Base Sepolia escrow, or the equivalent ed25519-verified instruction on the Solana devnet program: a pass releases to the seller, a fail refunds the buyer.OrderReleased / OrderRefunded on Base Sepolia, or the equivalent settled state on Solana devnet — the same verdict, either chain.Settlement: how one signed verdict releases money across three testnet chains
Grain's settlement layer sits behind a chain-neutral SettlementAdapter seam, and that
seam now has two implementations (EVM and Solana) — both consuming the same canonical
verdict, live across three chains. The EVM rail is a single verification-gated contract, GrainEscrow,
deployed on Base Sepolia (chainId 84532) at
0xEEb15426d656dCfb76AC058D1A1e66c821cB78d9 and on BNB Chain testnet (chainId 97) at
0x300Aa36E9b19F2ea4F9eDb6415788C7912e54162, settling
test USDC (on Base, Circle test USDC 0x036CbD53842c5426634e7929541eC2318f3dCF7e,
6 decimals). On the EVM rail the lifecycle is three moves:
- Fund. The buyer calls
fundOrder(escrowId, seller, grader, amount, specHash). This pullsamountUSDC in and records the order write-once:buyer = msg.sender, theseller, thegrader(the only signer whose verdict counts for this order), and thespecHash. - Grade + sign. Off-chain, the grader signs the verdict. The signature is EIP-191
personal_signover the digest below; the runtime reproduces it byte-for-byte, so the off-chain signature is exactly what the contract recovers. - Settle. Anyone — in practice a keeper — calls
settleOnAttestation(escrowId, result, assetHash, graderSignature). The contract recovers the signer and requires it to equal the order's recordedgrader, else it reverts withbad grader sig. On pass it paysseller; on fail it refundsbuyer.
digest = keccak256(abi.encode(
"Grain settleOnAttestation",
escrowId, uint8(result), assetHash,
address(this), // this exact contract
block.chainid // this exact chain (84532)
))
// signed EIP-191 personal_sign; recovered signer must == order.grader
- The keeper pays gas and has zero authority. Recipients are fixed on-chain, so it can't redirect funds; the runtime treats a reverted-but-included transaction as a hard failure (it waits for the receipt and throws unless
status === 'success'). - The signature can't be replayed. The digest binds the verdict to this order, this result, this exact asset (
assetHash), this contract, and this chain — a pass can't be replayed as a fail, asset A's signature can't settle asset B, and another instance or chain is rejected. Each binding has a dedicated test. - Hardened.
ReentrancyGuard+ checks-effects-interactions (state committed before transfer) + one-shot (theStatus.Fundedguard means a settled order can't settle twice).
Four autonomous rounds (fail → refund, pass → release, fail → refund, pass → release) ran
end-to-end against the Base Sepolia deployment with no human in the loop, plus the full Hardhat
suite covering the custody boundary, signature binding, one-shot, and reentrancy. The same EVM
contract is also live on BNB Chain testnet with a verified on-chain release. The Solana
devnet program is deployed and its settle path is verified on-chain via
npm run solana:settle (ed25519 verdict, same canonical tuple). (An existence
proof of the loop across all three chains, not a reliability statistic; the contracts have tests + live
testnet runs, not a third-party audit.)
The second rail — Solana devnet (ed25519)
The same canonical verdict also settles on Solana. An Anchor program in contracts-solana/
is deployed and live on Solana devnet at program id
4EDeeLJNbh9AXdLsfkgHswJ9VS2RX7yad9wV9pU7Ebgs, with a TypeScript adapter
(packages/runtime/src/solana-adapter.ts) that defaults to that program id. Instead of
recovering an EIP-191 signature to an address, the program verifies an ed25519
signature against the order's recorded grader pubkey. The signed message binds the same
fields — DOMAIN || order_id || result || asset_hash || program_id || cluster — so the
verdict is just as replay-bound as on EVM (a pass can't be replayed as a fail; another program or
cluster is rejected). You can prove the round-trip on devnet with
npm run solana:settle.
// EVM digest (recovered signer must == order.grader address)
digest = keccak256(abi.encode(
"Grain settleOnAttestation",
escrowId, uint8(result), assetHash,
address(this), block.chainid)) // this contract, chainId 84532
// Solana message (ed25519, verified against order.grader pubkey)
msg = DOMAIN || order_id || result || asset_hash || program_id || cluster
// one canonical verdict { escrowId, result, assetHash, graderSignature }
// → either adapter consumes it; the sign-only key can never pay itself on either chain
Mainnet path (roadmap, not built). On
mainnet the same EVM contract would point at bridged USDC and the Solana program at a mainnet
cluster; no mainnet deployment exists on either rail. Both adapters are real today — EVM live on
Base Sepolia and BNB Chain testnet, Solana live on devnet — and the SettlementAdapter seam means a third
rail is a new adapter, not a re-architecture.
Operating it: verify on-chain first, keep the audit trail, then revoke and stop
Everything you need to manage a live connection is verifiable on-chain — read first, trust nothing you haven't checked.
Verify status by on-chain reads
Read orders(escrowId) and the status byte: 0 None, 1 Funded,
2 Released, 3 Refunded. To confirm a settlement actually happened, watch
the events — OrderReleased and OrderRefunded each carry escrowId,
the recipient, the amount, the assetHash, and the settler (the keeper).
Don't report a settlement off the keeper's optimism; confirm it landed in state and in an event.
Keep the audit trail
assetHash— the sha256 of the exact GLB bytes the grader judged, baked into the signed digest and recorded in the event, so any payout is provably tied to one specific artifact.predicateDigest— a sha256 over the sorted, timestamp- and evidence-excluded predicate results, so re-grading the same asset later proves you got the same verdict.- Keep the
(escrowId, assetHash, predicateDigest, tx hash)tuple per order — that is your durable record.
Verify the grader, then revoke and stop
The order records the grader whose signature is the only one honored — an address on the
EVM rail, a pubkey on Solana. The public signer in the EVM deployment is
0xB89e1Aba9c23f8227f29d3727570526e3Dd7C5Ee — the public address (the Solana
rail records the grader pubkey analogously); the private key never leaves the grader and is never
in source or logs. Because the
contract has no owner, pause, or withdraw, you don't "turn off" the escrow — you
stop feeding it: stop the keeper (no keeper → no settlement), stop funding new orders, and fund any
future order with a rotated grader address. Drain funded orders first — stopping the keeper strands
an already-funded order until a valid grader signature settles it. Released/Refunded orders are
final. There is no sweep and no admin escape hatch — which is the point: no admin attack surface.
Troubleshooting
Never report a pass/fail or "settled" state you have not verified against a signed deterministic verdict and an on-chain receipt. If the grader has no deterministic tool evidence, it must not emit a verdict; if the settle transaction hasn't confirmed success, it must not report "settled."
| Symptom | Likely cause | What the Mind should check | Fix |
|---|---|---|---|
| A Mind never replies / a step hangs (no narration) | Cognition balance is 0 (silent timeout), or a fresh-Mind top-up charged without minting credits (balance is not real-time) | Hard-refresh the builder UI to read the real balance; was the top-up done immediately after creating the Mind? | Fund cognition and hard-refresh; if a fresh-Mind top-up didn't mint, fund again after the wallet registers. Narration is flavor-only — a Mind timeout must never block settlement (the signed verdict is the sole gate). |
| Verdict won't settle (tx reverts) | Bad/missing grader signature, wrong rail binding (EVM escrowAddress/chainId vs Solana programId/cluster), grader mismatch (address vs pubkey), or the order is already settled / not settleable |
Did grade_asset return a non-null signed verdict (an escrowId was supplied)? Does the verdict's chain binding match the rail you're settling on — EVM 0xEEb1…78d9 / 84532, or Solana program 4EDee…Ebgs / devnet? On-chain order status? |
Re-grade WITH the correct escrowId and the right rail binding for a fresh signature; don't retry a settle on a Released/Refunded order. A returned tx hash ≠ success — a non-success receipt (EVM) or failed instruction (Solana) is a hard failure. |
| Asset fails unexpectedly | Graded against the wrong profile (e.g. the strict private game-ready-prop@1 instead of the public sample-game-prop@1), missing UVs, an oversized texture, or declared params missing (named_params) — not always a topology defect |
The per-predicate evidence: uv_present UV channel; texture_size px vs budget; file_size bytes vs budget; tri_count vs budget; manifold boundary/non-manifold edges; valid_glb errors; named_params declared/required/missing. |
Confirm the right profileId (or pass a BYO inline profile), then fix the named predicate — unwrap UVs, resize the texture into budget, weld + simplify, or attach the required params. A genuine non-manifold or unwrapped asset should fail — that's the grader correctly saying "no." |
semantic_match fails or is borderline |
The asset doesn't match the intent, or the score is near the threshold (calibrated T ≈ 0.5–0.6; less-distinctive shapes like the barrel score ~75% — figures from the honest-v0 N=5 benchmark, which proves the method + floor, not the hard/confusable cases) | topMatch, intentScore, and the per-label scores in the evidence. |
If topMatch is a distractor, the fail is correct; if near-margin, treat it as low-confidence and say so. Do not raise the threshold to force a pass. |
| Skill not callable / tool errors | The Skill isn't equipped, or the Connection holding the grading key is missing/misconfigured | Is "Grain 3D Conformance Grader" equipped? Is the Connection attached and valid? Are secrets in Connections (not chat)? | Attach/repair the Connection, then re-grade a known profile. Skill edits apply to NEW sessions — start a fresh session to pick up a fixed version. |
| Skill behaves like an old version after an edit | Published Skills are live but versioned — in-flight sessions keep their version | Are you in the same session where the edit was made? | Start a new session to pick up the updated Skill (REGISTRY_Update / SKILL_Update apply to new sessions). |