scripts/local_e2e_tool_economy.py is the tool-substrate session's
capstone. It proves the whole tool-economy architecture — daemon,
substrate consensus, and chain — composes on a single local deployment,
with each hop verified against the next.
python scripts/local_e2e_tool_economy.pyPrerequisites (already true in this repo): npm install done
(node_modules/ present, incl. Hardhat), contracts compiled
(npx hardhat compile), and the Python deps installed
(web3, eth-account, the atn + nodes packages importable).
Windows and POSIX both supported; the script manages the Hardhat node
subprocess and kills it (taskkill /T on Windows) on teardown.
The script prints a stage-by-stage scoreboard and exits nonzero if any stage FAILs. A stage may legitimately SKIP (with a printed reason) rather than sink time — currently only the ServiceMarket channel leg is allowed to SKIP (it catches and notes any error rather than blocking).
Seven stages, each verified:
- Chain up — launches
npx hardhat node(localhost:8545), deploysSubstrate.sol+ServiceMarket.sol(ServiceRegistry + PaymentChannel, both taking the substrate address) via a tiny inlinehardhat rundeployer. Settlement is ATN-only, so no MockERC20 is deployed. Hardhat's fundedaccount[0]is the author's human OWNER wallet,account[1]the caller's. - Fleet — two real ATN
Runtimes in tmp dirs (autonet.enabled=False; we drive the substrate pieces directly). Runtime A hostsauthor-1(owner = account[0]); it also hostscaller-1(owner = account[1] on chain) so tool invocation stays in one daemon (tools are daemon-local) while the two DISTINCT on-chain owners the damper needs still exist. Runtime B stands up its own agent to prove the two-runtime fleet. Verifiesauthor-1's parentless lineage is owner-rooted (hashes from the owner wallet). - Chain identity — owner-bound registration on chain. Reads each
agent's key from
runtime.registry.get_agent_key, funds the agent address with gas from its owner, signs the owner-binding EIP-712, and calls the registration method from the agent's own key. VerifiesgetAgentOwnerreturns the two distinct owners on chain. - Tool economy (daemon) —
author-1registers a pinned echo tool (register_tool), thenpublish_tools it (the production two-step publish gate);caller-1invokes it viatool_registry.call_tooland then runsattest_tools(the cognitive attestation — the only mint-counting usage). Two validators thenvet_toolit: they first read the pinned code back, then attest a pass verdict with a report. Under v4.1 (vet gate retired) a vet is an INSPECTION REVIEW — it drifts position but mints nothing and gates nothing. AWorldServiceis wired to the tool_store'smanifest_sink/event_sinkexactly likeatn/autonet_service.pydoes; the tool_used receipts are captured off the event sink and the registration sprout is read from the WorldService epoch buffer. - Consensus — the REAL federated close. Registration sprouts (author
signing key) and receipts (caller signing key) become canonical
EventBatchchains, driven throughFederatedCloseDriver.run()with a mocked gossip (thetests/test_tool_mintTestDriverCarryOver pattern). Theagent_owner_mapis read back from chain (getAgentOwnerfor both agents), keyed by the same 0x addresses the events now carry. Verifies the mint creditsauthor-1,attesters == 1, and — under v4.1 — that the vet gate is removed: the tool mints from first attested use, the author keeps 100% (no validator royalty), and the inspection-review vets earn nothing. Also a control case: setting the caller's owner equal to the author's in the map drops the mint to zero — proving the chain-fed same-owner exclusion. - Settlement — the rotating submitter anchors the epoch
(
submitAnchorviaEpochAnchorer),author-1records its own mint (recordTrainingForEpochviaAuthoritativeChainSubmitter, mint-merkle proof against the anchored root). Money only (Decision 2026-07-10): Substrate has no reputation surface — the mint is a 2-field(agent, amount)leaf. Verifies the cumulative tool-pool earnings ledger (agentMintTotal) and the ATNbalanceOfboth moved on chain by the scaledagent_mint[author]amount (REP is claimed DAO-side, not minted here). Then registers one Service onServiceRegistryand runs onePaymentChannelround-trip in ATN (client opens a deposit → signs one EIP-712 voucher → provider closes → provider balance +ask NET of the 2.5% service fee, routed throughpayForService; remainder refunds fee-free after the challenge window). - Teardown — kills the Hardhat node, removes tmp dirs, prints the scoreboard.
[PASS] Stage 1 — chain up (hardhat node + deploy)
[PASS] Stage 2 — fleet (two ATN runtimes, owner-rooted lineage)
author_lineage_root=0xf39Fd6e5… author_addr=0x…
[PASS] Stage 3 — chain identity (owner-bound registration)
author_owner_onchain=0xf39Fd6e5… caller_owner_onchain=0x7099797…
distinct_owners=True
[PASS] Stage 4 — tool economy (register + invoke + attest + vet)
registration_events=1 attested_receipts=1 vets=2
[PASS] Stage 5 — consensus (federated close + owner-map exclusion)
tool_mint_raw=4.15888308 agent_mint_raw=8.15888308
vet_gate=removed (v4.1); author keeps 100%
control_same_owner_mint=0 (excluded)
[PASS] Stage 6 — settlement (anchor + recordTraining + channel)
earnings_delta=8158883 atn_delta=8158883
service_id=1 channel_provider_delta=975 channel_client_refund=2000
[PASS] Stage 7 — teardown
7 PASS / 0 FAIL / 0 SKIP
These are the composition frictions found while wiring the loop end-to-end. None block the loop (the script works around each), but each is a real observation about where the pieces don't yet click cleanly.
-
Registration sprout carries
author_post=False→ zero-standing tool mint.WorldService.submit_tool_manifestemits the registrationSubClaimSproutedwithauthor_post=False. In the default ledger-pricing federated close (no equilibration), the manifest claim node then has zeronet_scorestanding, somint = standing × usage_term = 0— a freshly-registered, genuinely- attested pinned tool mints NOTHING. The unit fixture (tests/test_tool_mint._registration_event) hand-setsauthor_post=Trueto get standing 1, which is why the tests pass while the live daemon path would mint zero until standing accrues organically. The script setsauthor_post=Trueon the buffered registration events (documented inline) to exercise the mint. Open question for the owner: should tool registration stamp an author post (giving the manifest immediate unit standing), or is a tool author supposed to earn standing only through subsequent review activity? This is the single most load-bearing seam — the whole "tools mint to their author" claim hinges on it. (v4.1 note: usage mints from first attested use regardless, so this seam is now about ledger-pricing standing math, not the mint gate — the vet gate it once interacted with is retired.) -
Tool registration events don't reach the
event_sink. Onlytool_usedreceipts flow throughtool_store.event_sink; the manifest registration goes throughmanifest_sink → submit_tool_manifest, whose sprout lands in the WorldService's epoch buffer, not the sink. So a consumer that only tapsevent_sink(the natural place to collect "everything for the epoch batch") is missing the registration and gets an empty mint. The script reads registration events out ofws._epoch_eventsinstead. A single unified "all substrate events for this epoch" tap would remove this footgun. -
String agent-id vs 0x address at the mint→chain boundary. The tool events carry the daemon-local agent id (
author-1,caller-1) inauthor/tool_author/author_agent.federated_epoch_closekeysagent_mintby that string. Butmint_merkleonly admits keys that parse as 0x addresses (the contract can only verifymsg.sender), so a string-keyed mint has no on-chain claim path — it silently drops at the merkle-leaf stage. Production bridges this withworld_substrate_feed.ResolvedAgentIdentity(local id → 0x). The script replicates that remap explicitly before building the canonical batches. The seam: nothing in the tool-mint path itself enforces or documents that the author field must be an address by the time it reaches the anchor; it's an implicit contract between the feed resolver and the merkle builder. -
On-chain amount is
agent_mint[author], nottool_mint[digest]. The tool-author mint is merged into the broaderagent_mintmap (through the violator-pays gate) BEFORE anchoring, so the amount that hits the chain (8.15…) is larger than the standalone tool_mint entry (4.15…). Verifying the chain delta againsttool_mint[digest]fails; the correct source isagent_mint[author]. Obvious in hindsight, but a trap for anyone reasoning "the tool minted X, so X should appear on chain." -
Hardhat node's
chainIdis 1337, not 31337.npx hardhat nodeserves the in-processhardhatnetwork (hardhat.config.js:chainId: 1337), while the daemon's chain config defaults to31337(the eth_tester default). Signing txs for 31337 gets rejected with "incompatible EIP-155 transaction, signed for another chain." The script pinsCHAIN_ID = 1337. Worth noting for anyone pointing the real daemon at a localhardhat node. -
Hardhat node stdout must be drained.
npx hardhat nodelogs every RPC call; if its stdout PIPE buffer fills, the process blocks and the deploy side sees an undiciHeadersTimeoutError. The script sends the node's stdout/stderr to DEVNULL. (A subtle subprocess-management trap, not an architecture seam, but it cost a run.) -
Owner-binding EIP-712 is under active rename. During this work the contract's owner-binding struct was renamed Sponsorship → OwnerBinding (
registerAgentSponsored→registerAgentBound,sponsorshipHash→bindingHash,sponsorshipNonce→bindingNonce, eventAgentSponsored→OwnerBound) and gained a mandatorynoncein the typed struct. The script is written to survive either shape: it discovers the method/view/event names by ABI introspection and — rather than reconstruct the typed-data struct itself — asks the contract's ownbindingHash/sponsorshipHashview for the exact digest and signs that raw 32-byte digest with the owner key. This sidesteps all struct-field/typehash drift: whatever the contract hashes is what gets signed. Any future field addition to the binding struct needs no script change as long as a hash view is exposed.
# Local end-to-end: the venture loop composes
`scripts/local_e2e_venture_loop.py` is the sibling capstone for the
venture-loop contracts + rails (VentureVault + factory, CharterAnchor,
TrialRunner, the evidence CON rail). It **reuses the tool-economy
harness** wholesale (`start_hardhat`, `deploy_contracts`,
`resolve_binding_shape`/`sponsored_register`, `send_agent_tx`,
`make_runtime`, `register_local_agent`, the `Board` scoreboard and
`_fake_embedder`) and bolts CharterAnchor + VentureVaultFactory onto the
base deploy. It stands up ONE daemon runtime, registers a fleet of
owner-bound agents against a live Hardhat chain, and drives the whole
venture lifecycle end-to-end.
## Run
```bash
python scripts/local_e2e_venture_loop.py
Same prerequisites as the tool-economy e2e (npm install,
npx hardhat compile, Python deps). --from-stage / --to-stage
letter flags exist for iteration, but stages A–F share on-chain + daemon
state, so a real green run uses no flags.
Eight stages (0 bootstrap + A–G), each verified:
- 0 chain up — base deploy (Substrate + ServiceMarket; ATN-only
settlement, no MockERC20) plus a second inline deployer for
CharterAnchor(governor =account[0]) andVentureVaultFactory. - A CharterAnchor — anchor the live
charter_hash()(v1), assert the daemon'sverify_charter_against_anchorMATCHES; anchor a bogus hash (v2, append-only) and assert the drift-warning path fires (match=False); re-anchor the real hash (v3) and assert current matches again. - B venture — the venture agent publishes a
venture_prospectusblob (echo battery,pass_threshold=1.0); a VentureVault is deployed via the factory bound to that agent (termsBps=6000, the 60% backer split). - C backers — two funded accounts mint ATN via the substrate's only
mint path (single-leaf
recordTrainingForEpoch), approve + contribute toraiseMin,openTrials. - D trials — two verifier agents (distinct owners) run
TrialRunner.run_trialagainst the prospectus through a local echo transport, submitattestTrialon chain from their own keys; greenlight; a verifier claims its fee slice. - E tranches + revenue — agent claims tranche 1; a customer pays via
payForService(vault, …), which takes the 2.5% service fee at payment time so the vault receives the net (975 of 1000);claimRevenuesplits that net 60/40; backers claim pro-rata within the 60% (351/234, asserted to the wei); a ≥1/3 backer halts the next tranche (asserted unclaimable), un-halts, and the tranche is claimable again. - F evidence rail — the author publishes a deliberately-buggy pinned
tool; a disputer files an evidence-bearing CON; a third daemon runs
check_evidence→ confirms → posts support. Two mini-closes (with vs without recruited support) show the supported CON's standing is double the naked CON's (52 vs 26) and drags the disputed tool's net_score strictly further negative (+2 → −24) — the close prices the CON above the naked equivalent. - G — totals + teardown (Hardhat killed, tmp + deploy records removed).
Additional to the tool-economy seams above:
-
close_epoch(apply_gate=False)leavesnode_mintempty for a single-epoch evidence comparison. The reliable pricing signal for "did the recruited support move standing" is the LIVEnet_scoreread viaWorldService.read_node_scores()(equilibrated state), not the close record'snode_mintdiagnostic — which stays 0.0 for a graph that has not accumulated an emission-priced usage term. The script asserts onread_node_scores()(CON node standing and the disputed tool's net_score) and keeps the mini-closenode_mintfigures as labeled diagnostics only. -
Tranche auto-vesting from tx latency makes the halt boundary nondeterministic. With a short
tranchePeriod(seconds), the real block-time that elapses across the greenlight → claim → halt tx sequence silently vests a second tranche, sohaltPeriodVestedsnapshots past the tranche the test wants to freeze. The script uses a largetranchePeriod(100_000s) and drives vesting purely withevm_increaseTime, so the halt boundary is exact. -
submit_tool_manifestfails SILENTLY ({"error": …}, nomanifest_digest) on an incomplete manifest. Seeding a fresh WorldService with the disputed tool's claim node needs a FULLY valid manifest —kind: "tool_manifest"and (for pinned) a non-emptycode_digest— orvalidate_manifestrejects it and the receipt has nomanifest_digestkey. Easy to miss because the daemon path builds the manifest for you; hand-building one for a directsubmit_tool_manifestcall must satisfy_REQUIRED_FIELDS. -
CharterAnchor is append-only, so "fix the drift" is a NEW version. Re-anchoring the correct charter hash after a bogus anchor does not edit v2 — it appends v3.
currentCharter()then tracks v3, andversionCountis 3. The forward-only doctrine (no rollback) is load-bearing: the test assertschain_version == 3after the realignment, not a return to v1. -
The scoreboard's arrow/box glyphs crash a cp1252 Windows console. The final
print(board.render())carries→and═; on a stock Windows console (cp1252) that raisesUnicodeEncodeError. The script reconfiguressys.stdout/sys.stderrto utf-8 before running.