A decentralized marketplace where model creators mint their weights as NFTs, compute providers stake to serve inference, and every call settles on-chain.
Synapse is a full-stack on-chain model marketplace on Ethereum Sepolia. Model creators publish encrypted weights to IPFS, mint an ERC-721 that represents ownership and pricing, and split revenue with up to ten collaborators. Users pay per-token or subscribe, and independent compute nodes stake ETH, serve the HuggingFace pipeline locally, and settle token counts + result hashes back to the contract.
Model creators hand their weights to closed APIs and lose provenance, pricing control, and any downstream revenue. Buyers cannot prove which model served their request, cannot audit who got paid, and cannot take their access with them. Compute providers have no neutral market to monetize spare GPUs without signing vendor contracts. Every existing "AI marketplace" is a billing UI on top of someone else's cluster — ownership, settlement and provenance are off-chain.
A monorepo with six Solidity contracts, a two-role FastAPI backend (orchestrator + compute node), a Ponder v7 indexer, a Lit Protocol v7 encryption sidecar, and a Next.js 15 frontend. Every paid inference emits InferenceRequested / InferenceSettled, every mint emits ModelMinted, and every payout accrues to pendingWithdrawals for pull-payment.
| Capability | Implementation |
|---|---|
| Model ownership as an NFT with versioning | packages/hardhat/contracts/ModelNFT.sol (mintModel, addVersion) |
| Per-call inference escrow + pull-payment settlement | packages/hardhat/contracts/Marketplace.sol (purchaseInference, settleInference, withdraw) |
| Subscriptions with monotonic expiry | Marketplace.subscribe + subscriptionExpiry mapping |
| Staked compute nodes with weighted selection | ComputeRegistry.sol (registerNode, selectNode) |
| Deposit account with daily/monthly spend caps | UserWallet.sol (deposit, pay, setLimits) |
| Per-token encryption ACL | services/lit-agent/src/server.ts (buildACL → Lit v7 evmContractConditions) |
| IPFS pinning with multi-gateway fallback | backend/shared/ipfs.py (Pinata v3 + dweb.link + ipfs.io) |
| On-chain event timeline | backend/orchestrator/routes_events.py (eth_getLogs over ModelNFT + Marketplace) |
| Real HuggingFace pipeline execution | backend/node/inference.py + backend/node/model_cache.py |
| Soulbound achievement badges | BadgeSBT.sol (ERC-5192, checkAndAward) |
| Ratings tied to real usage | Reputation.sol (onInferenceServed, rateModel) |
| GraphQL indexer of every contract event | packages/ponder/src/index.ts |
| Cross-machine proof-of-inference miner | backend/orchestrator/routes_mining.py + components/mine/MiningDashboard.tsx |
graph TD
subgraph Frontend_Next_js_15
Landing[app/page.tsx]
AppShell[app/app/layout.tsx]
ModelPage[app/app/model/id/page.tsx]
UploadPage[app/app/upload/page.tsx]
NodesPage[app/app/nodes/page.tsx]
WalletCtx[lib/wallet.tsx::WalletProvider]
ApiClient[lib/api.ts]
ContractsClient[lib/contracts.ts]
PonderClient[lib/ponder.ts]
end
subgraph Backend_FastAPI
Orchestrator[backend/main.py ROLE=orchestrator]
ComputeNode[backend/main.py ROLE=node]
RoutesInfer[routes_infer.py]
RoutesModels[routes_models.py]
RoutesPlatform[routes_platform.py]
RoutesEvents[routes_events.py]
RoutesNodes[routes_nodes.py]
RoutesMining[routes_mining.py]
NodeExecute[node/routes_node.py::/node/execute]
end
subgraph Blockchain_Sepolia
ModelNFT[ModelNFT.sol]
Marketplace[Marketplace.sol]
ComputeRegistry[ComputeRegistry.sol]
UserWallet[UserWallet.sol]
Reputation[Reputation.sol]
BadgeSBT[BadgeSBT.sol]
end
subgraph Storage
Pinata[Pinata v3 uploads.pinata.cloud/v3/files]
Gateways[IPFS gateways CSV fallback]
Ponder[Ponder v7 GraphQL localhost:42069]
end
subgraph Compute_Lit
LitAgent[services/lit-agent/src/server.ts]
HFCache[node/model_cache.py]
Pipeline[node/inference.py::load_pipeline_from_metadata]
end
UploadPage -->|uploadModel| ApiClient
ApiClient -->|POST /api/models/upload| RoutesModels
RoutesModels -->|pin_file/pin_json| Pinata
RoutesModels -->|encrypt_for_token| LitAgent
RoutesModels -->|totalSupply| ModelNFT
UploadPage -->|mintModel| ContractsClient
ContractsClient -->|mintModel CIDs| ModelNFT
ModelPage -->|purchaseInference| ContractsClient
ContractsClient -->|purchaseInference| Marketplace
Marketplace -->|selectNode pipelineTag| ComputeRegistry
Marketplace -->|credit refund| UserWallet
ModelPage -->|infer| ApiClient
ApiClient -->|POST /api/infer| RoutesInfer
RoutesInfer -->|sessions sessionId| Marketplace
RoutesInfer -->|node endpoint| ComputeRegistry
RoutesInfer -->|POST /node/execute| NodeExecute
NodeExecute -->|ensure_model_local| HFCache
HFCache -->|fetch_cid| Gateways
NodeExecute -->|decrypt| LitAgent
NodeExecute -->|load_pipeline_from_metadata| Pipeline
NodeExecute -->|settleInference| Marketplace
Marketplace -->|onInferenceServed| Reputation
Marketplace -->|checkAndAward| BadgeSBT
ModelPage -->|GET /platform/models/id/events| RoutesEvents
RoutesEvents -->|eth_getLogs| ModelNFT
RoutesEvents -->|eth_getLogs| Marketplace
NodesPage -->|GET /api/nodes/probe| RoutesNodes
RoutesNodes -->|nodeList node| ComputeRegistry
PonderClient -->|GraphQL| Ponder
Ponder -->|indexes events| ModelNFT
Ponder -->|indexes events| Marketplace
Ponder -->|indexes events| ComputeRegistry
Ponder -->|indexes events| UserWallet
Ponder -->|indexes events| BadgeSBT
Ponder -->|indexes events| Reputation
RoutesPlatform -->|fallback queries| Ponder
Frontend (app/, components/, lib/) runs on Next.js 15 with the App Router. lib/wallet.tsx owns the window.ethereum session, caches a SIWE token for 240 s, and hands both to lib/contracts.ts (writes via viem walletClient) and lib/api.ts (reads + SIWE-gated downloads to the orchestrator). lib/ponder.ts is a thin GraphQL client over the indexer for live counters.
Backend (backend/) ships one FastAPI app selected at import time by the ROLE env var. ROLE=orchestrator mounts routes_models, routes_infer, routes_platform, routes_events, routes_nodes, routes_mining. ROLE=node mounts routes_node (/node/execute, /node/info, /node/health). All writes use web3.py 7.5 against the Alchemy Sepolia RPC.
Blockchain (packages/hardhat/contracts/) is Solidity 0.8.26 on top of OpenZeppelin v5. Six contracts (ModelNFT, Marketplace, ComputeRegistry, UserWallet, Reputation, BadgeSBT) deployed to Sepolia via hardhat-deploy scripts in packages/hardhat/deploy/. Wiring is done in 04_Wiring.ts and is idempotent.
Storage. Weights and metadata JSON are pinned via Pinata v3 (backend/shared/ipfs.py::pin_file) and read through the CSV fallback chain in IPFS_GATEWAY_URLS. Event data is mirrored into Ponder (packages/ponder/src/index.ts) for sub-second UI aggregations; the backend falls back to direct eth_getLogs when Ponder lags.
Compute. services/lit-agent/ is a Node.js sidecar that wraps Lit Protocol v7 (datil-dev) and enforces a per-token ACL of Marketplace.hasAccess(tokenId, :userAddress) OR ComputeRegistry.isActiveNode(:userAddress). Nodes run backend/node/inference.py which materialises HuggingFace pipelines from a local cache.
sequenceDiagram
autonumber
participant User as EOA (MetaMask)
participant UP as app/upload/page.tsx
participant API as backend/routes_models.py
participant LIT as lit-agent/server.ts
participant IPFS as Pinata v3
participant MNFT as ModelNFT.sol
participant MP as Marketplace.sol
participant CR as ComputeRegistry.sol
participant NODE as node/routes_node.py
participant REP as Reputation.sol
participant SBT as BadgeSBT.sol
rect rgb(245,245,245)
Note over User,SBT: Flow A — Developer publishes a model
User->>+UP: select file + metadata
UP->>+API: POST /api/models/upload (multipart)
API->>+MNFT: totalSupply() (predict next tokenId)
MNFT-->>-API: n
API->>+LIT: POST /encrypt {dataB64, tokenId=n+1, addresses}
LIT-->>-API: {ciphertext, dataToEncryptHash, evmContractConditions}
API->>+IPFS: POST /v3/files (ciphertext)
IPFS-->>-API: modelCID
API->>+IPFS: POST /v3/files (metadata.json)
IPFS-->>-API: metadataCID
API-->>-UP: {modelCID, metadataCID, predictedTokenId}
UP->>+MNFT: mintModel(modelCID, metadataCID, framework, pipelineTag, license, tags)
MNFT->>SBT: checkAndAward(creator, tokenId)
MNFT-->>-UP: tokenId
UP->>+MP: list(tokenId, ppIn, ppOut, subDay)
MP-->>-UP: ok
end
rect rgb(240,240,245)
Note over User,SBT: Flow B — Buyer pays and runs inference
User->>+MP: purchaseInference(tokenId, maxTokens) {value: ceilingCost}
MP->>CR: selectNode(pipelineTag)
CR-->>MP: nodeAddress
MP->>CR: bumpOpenSessions(nodeAddress, true)
MP-->>-User: sessionId
User->>+API: POST /api/infer {sessionId, tokenId, prompt, user}
API->>MP: sessions(sessionId)
API->>CR: node(selectedNode)
API->>+NODE: POST /node/execute
NODE->>MP: sessions(sessionId)
NODE->>NODE: ensure_model_local + run HF pipeline
NODE->>+MP: settleInference(sessionId, in, out, latencyMs, resultHash)
MP->>MP: _distribute(tokenId, actualCost, node)
MP->>REP: onInferenceServed(tokenId, user)
MP->>CR: recordCall(node, true, latencyMs)
MP->>CR: bumpOpenSessions(node, false)
MP->>SBT: checkAndAward(user, tokenId)
MP-->>-NODE: ok
NODE-->>-API: {outputStr, tokens, settlementTxHash, resultHash}
API-->>-User: InferenceResponse
User->>+MP: withdraw()
MP-->>-User: ETH
end
| Contract | Purpose | Key Functions |
|---|---|---|
ModelNFT.sol |
ERC-721 ownership + versioning of models | mintModel, mintModelFor, addVersion, model, versions, withdraw |
Marketplace.sol |
Listings, escrow, settlement, pull-payment distribution | list, subscribe, purchaseInference, settleInference, reclaimExpiredSession, setFees, withdraw |
ComputeRegistry.sol |
Node staking + weighted node selection | registerNode, deregister, selectNode, recordCall, bumpOpenSessions, node |
UserWallet.sol |
Credit account + daily/monthly limits + sponsored calls | deposit, withdraw, setLimits, pay, credit, spendAndCheck, setSponsoredTarget |
Reputation.sol |
Usage-gated ratings | onInferenceServed, rateModel, averageRating, usageCount |
BadgeSBT.sol |
Soulbound ERC-5192 achievement badges | checkAndAward, awardEarlyAdopter, locked, burnBadge |
- Purpose: Mint and version models as ERC-721 tokens. Non-burnable.
- Inherits from:
ERC721,ERC721Enumerable,ERC721URIStorage,Ownable,ReentrancyGuardTransient,IModelNFT.
| Function | Visibility | Parameters | Returns | Description |
|---|---|---|---|---|
mintModel |
external payable | string modelCID, string metadataCID, bytes32 framework, bytes32 pipelineTag, bytes32 license, bytes32[] tags_ |
uint256 tokenId |
Mint a new model NFT to msg.sender. Requires msg.value >= mintFee (default 0.001 ether). |
mintModelFor |
external payable | address creator, string modelCID, string metadataCID, bytes32 framework, bytes32 pipelineTag, bytes32 license, bytes32[] tags_ |
uint256 tokenId |
Sponsored mint; only callable by wired UserWallet. |
addVersion |
external | uint256 tokenId, string modelCID, string metadataCID, string changelog |
— | Append a version to an existing model; only NFT owner. |
withdraw |
external | — | — | Claim pendingWithdrawals[msg.sender] via pull-payment. |
setBadgeSBT |
external onlyOwner | IBadgeSBT b |
— | Wire the BadgeSBT contract. |
setTreasury |
external onlyOwner | address payable t |
— | Update fee sink. |
setMintFee |
external onlyOwner | uint96 f |
— | Update the flat mint fee. |
setUserWallet |
external onlyOwner | address w |
— | Enable/disable mintModelFor. |
model |
external view | uint256 tokenId |
ModelMetadata |
Latest metadata struct for a token. |
versions |
external view | uint256 tokenId |
Version[] |
Full version history. |
versionsPaged |
external view | uint256 tokenId, uint256 offset, uint256 limit |
Version[] |
Paginated history. |
latestMetadataCID |
external view | uint256 tokenId |
string |
Top-level metadata CID. |
tags |
external view | uint256 tokenId |
bytes32[] |
Tags set at mint. |
Events: ModelMinted(uint256,address,string,string,bytes32,bytes32,bytes32), VersionAdded(uint256,uint256,string,string,string), ModelTagged(uint256,bytes32).
- Purpose: Listing, subscription, per-call escrow, settlement, and pull-payment distribution.
- Inherits from:
Ownable,Pausable,ReentrancyGuardTransient.
| Function | Visibility | Parameters | Returns | Description |
|---|---|---|---|---|
list |
external whenNotPaused | uint256 tokenId, uint96 ppIn, uint96 ppOut, uint96 subDay |
— | NFT owner lists a model with per-token and per-day prices. |
unlist |
external | uint256 tokenId |
— | NFT owner removes the listing. |
setCollaborators |
external whenNotPaused | uint256 tokenId, address[] payees, uint16[] bps |
— | Revenue split in basis points; sum must equal 10000; up to 10 payees. |
collaboratorsOf |
external view | uint256 tokenId |
(address[], uint16[]) |
Current split. |
subscribe |
external payable whenNotPaused nonReentrant | uint256 tokenId, uint32 daysCount |
— | Buyer pays daysCount * subscriptionPricePerDay. |
subscribeFor |
external payable onlyUserWallet | address user, uint256 tokenId, uint32 daysCount |
— | Sponsored subscription. |
hasActiveSubscription |
public view | uint256 tokenId, address user |
bool |
— |
quote |
external view | uint256 tokenId, uint32 maxTokens |
uint256 |
Ceiling cost for a per-call session. |
purchaseInference |
external payable whenNotPaused nonReentrant | uint256 tokenId, uint32 maxTokens |
bytes32 sessionId |
Escrow ceiling cost and open a session. |
purchaseInferenceFor |
external payable onlyUserWallet | address user, uint256 tokenId, uint32 maxTokens |
bytes32 sessionId |
Sponsored per-call. |
settleInference |
external nonReentrant | bytes32 sessionId, uint32 inputTokens, uint32 outputTokens, uint32 latencyMs, bytes32 resultHash |
— | Only assigned node; settles, distributes, refunds, awards badges. |
reclaimExpiredSession |
external nonReentrant | bytes32 sessionId |
— | Anyone; refunds full escrow after SESSION_TIMEOUT (1h) and slashes node success rate. |
withdraw |
external nonReentrant | — | — | Claim pendingWithdrawals[msg.sender]. |
hasAccess |
external view | uint256 tokenId, address user |
bool |
Ownership or active sub — used by Lit ACL. |
setFees |
external onlyOwner | uint16 _protocolFeeBps, uint16 _nodeFeeBps |
— | Rate-limited: sum ≤ 3000 bps, each step ≤ 100 bps up. |
setTreasury |
external onlyOwner | address payable t |
— | — |
pause / unpause |
external onlyOwner | — | — | Gate new spend; unwinds stay open. |
Events: ModelListed, CollaboratorsSet, Subscribed, InferenceRequested, InferenceSettled, Distributed, Withdrawable, FeesChanged.
- Purpose: Stake-gated node registry with weighted selection on
pipelineTag. - Inherits from:
Ownable,ReentrancyGuardTransient.
| Function | Visibility | Parameters | Returns | Description |
|---|---|---|---|---|
registerNode |
external payable | string endpoint, bytes32[] tasks |
— | Stake ≥ MIN_STAKE (0.001 ETH). |
registerNodeFor |
external payable onlyUserWallet | address operator, string endpoint, bytes32[] tasks |
— | Sponsored node registration. |
deregister |
external nonReentrant | — | — | Refund stake; reverts while openSessions > 0. |
selectNode |
external view | bytes32 pipelineTag |
address best |
Weighted score over up to 50 candidates. |
recordCall |
external onlyMarketplace | address node_, bool success, uint32 latencyMs |
— | EWMA latency + success counters. |
bumpOpenSessions |
external onlyMarketplace | address node_, bool inc |
— | — |
isActiveNode |
external view | address a |
bool |
— |
node |
external view | address a |
Node |
Full tuple used by the orchestrator. |
nodeListLength |
external view | — | uint256 |
— |
setMarketplace |
external onlyOwner | address mp |
— | — |
setUserWallet |
external onlyOwner | address w |
— | — |
Events: NodeRegistered, NodeDeregistered, CallRecorded.
- Purpose: ETH credit balance with per-user daily/monthly spend caps and a
pay(target, amount, data)allow-listed relay. - Inherits from:
Ownable,ReentrancyGuardTransient.
| Function | Visibility | Parameters | Returns | Description |
|---|---|---|---|---|
deposit |
external payable | — | — | Top up caller balance. |
withdraw |
external nonReentrant | uint256 amount |
— | Drain caller balance. |
setLimits |
external | uint128 daily, uint128 monthly |
— | 0 means unlimited. |
pay |
external nonReentrant | address target, uint256 amount, bytes data |
bytes ret |
Call target with value: amount; records spend. Target must be in sponsoredTargets. |
credit |
external payable onlyMarketplace | address user, uint256 amount |
— | Refund path used by Marketplace. |
spendAndCheck |
external onlyMarketplace | address user, uint256 amount |
— | Window bookkeeping for legacy EOA path. |
setMarketplace |
external onlyOwner | address mp |
— | Auto-allow-lists the marketplace. |
setSponsoredTarget |
external onlyOwner | address target, bool allowed |
— | Manage pay() allow-list. |
remainingDaily / remainingMonthly |
external view | address user |
uint256 |
— |
Events: Deposited, Withdrew, Credited, LimitsSet, WalletSpend, SponsoredTargetSet, MarketplaceSet.
- Purpose: 1–5 star ratings gated on actual inference usage.
- Inherits from:
Ownable.
| Function | Visibility | Parameters | Returns | Description |
|---|---|---|---|---|
onInferenceServed |
external onlyMarketplace | uint256 tokenId, address user |
— | Marks hasUsed and increments usageCount. |
rateModel |
external | uint256 tokenId, uint8 score |
— | Caller must have hasUsed, one rating per user. |
averageRating |
external view | uint256 tokenId |
uint256 |
Returns average × 100. |
totalRatings / sumRatings / usageCount |
external view | uint256 tokenId |
uint32 / uint64 / uint64 |
— |
setMarketplace |
external onlyOwner | address mp |
— | — |
Events: ModelRated.
- Purpose: Soulbound (ERC-5192) achievement badges.
- Inherits from:
ERC721,ERC721URIStorage,Ownable,IERC5192.
| Function | Visibility | Parameters | Returns | Description |
|---|---|---|---|---|
checkAndAward |
external | address user, uint256 tokenId |
— | Callable by Marketplace, ModelNFT, or owner; idempotent per (user, kind). |
awardEarlyAdopter |
external onlyOwner | address user |
— | Seed-time grant. |
burnBadge |
external onlyOwner | uint256 tokenId |
— | Demo repair; has is preserved. |
locked |
external pure | uint256 |
bool |
Always true (ERC-5192). |
setMarketplace |
external onlyOwner | address mp |
— | — |
Events: BadgeAwarded(address,uint8,uint256), Locked(uint256).
Kinds: FIRST_MODEL=0, TEN_MODELS=1, HUNDRED_INFERENCES_SERVED=2, TOP_RATED=3, EARLY_ADOPTER=4.
flowchart LR
DEV[Developer: app/upload/page.tsx] -->|multipart file + metadata_json| ORCH[backend orchestrator routes_models.upload_model]
ORCH -->|_validate_metadata pipeline_tag + tags regex| VAL{valid}
VAL -->|no| ERR[HTTP 400]
VAL -->|yes| PRED[_predict_next_token_id modelnft.totalSupply + 1]
PRED -->|encrypt true| LIT[services lit-agent POST /encrypt]
LIT -->|Lit v7 encryptString evmContractConditions ACL| CIPHER[ciphertext + dataToEncryptHash]
CIPHER -->|pin_file ciphertext.enc| PINATA1[Pinata v3]
PINATA1 -->|data.cid| MCID[modelCID]
ORCH -->|synapse.lit_data_hash and model_cid| META[metadata.json]
META -->|pin_json| PINATA2[Pinata v3]
PINATA2 -->|data.cid| MDCID[metadataCID]
MCID --> RET[UploadResponse]
MDCID --> RET
RET -->|ContractsClient.mintModel| MNFT[ModelNFT.sol]
MNFT -->|emit ModelMinted| PONDER[Ponder v7 model table]
- Frontend sends the weights blob + metadata JSON to
POST /api/models/upload. _validate_metadataenforces the pipeline tag + license + tags whitelist fromroutes_models.py._predict_next_token_idcallsModelNFT.totalSupply()and the predicted id (n+1) is baked into the Lit ACL so a race losing the mint triggers a clean re-upload.- If
encrypt=trueandLIT_KILL_SWITCHis false,encrypt_for_tokenPOSTs to the Lit sidecar, which buildsevmContractConditions = [hasAccess(tokenId, :userAddress), or, isActiveNode(:userAddress)]and encrypts the base64 payload. - The ciphertext is pinned via Pinata v3 (
uploads.pinata.cloud/v3/files,network=public) and the resulting CID is embedded inmetadata.synapse.model_cid. - The metadata JSON is pinned as a second object and returned as
metadataCID. - Frontend calls
ModelNFT.mintModel(modelCID, metadataCID, framework, pipelineTag, license, tags)from the user's wallet — emittingModelMinted(tokenId, creator, modelCID, metadataCID, pipelineTag, framework, license). Ponder indexes the event into themodeltable.
sequenceDiagram
autonumber
participant U as User (frontend)
participant MP as Marketplace.sol
participant CR as ComputeRegistry.sol
participant ORCH as orchestrator/routes_infer.py
participant NODE as node/routes_node.py
participant LIT as lit-agent/decrypt
participant HF as HuggingFace pipeline
participant REP as Reputation.sol
participant SBT as BadgeSBT.sol
U->>MP: purchaseInference(tokenId, maxTokens)
MP->>CR: selectNode(pipelineTag)
CR-->>MP: bestNode
MP->>CR: bumpOpenSessions(bestNode, true)
MP-->>U: sessionId (InferenceRequested)
U->>+ORCH: POST /api/infer {sessionId, tokenId, prompt, user}
ORCH->>MP: sessions(sessionId)
ORCH->>CR: node(bestNode)
ORCH->>+NODE: POST /node/execute
NODE->>MP: sessions(sessionId)
NODE->>NODE: ensure_model_local(tokenId, metadata)
alt encrypted
NODE->>LIT: POST /decrypt (operator self-auth)
LIT-->>NODE: plaintextB64
end
NODE->>HF: pipeline(prompt, max_new_tokens=maxTokens)
HF-->>NODE: raw result
NODE->>NODE: extract_output_text + count_tokens + keccak resultHash
NODE->>+MP: settleInference(sessionId, in, out, latencyMs, resultHash)
MP->>MP: _distribute(tokenId, actualCost, node)
MP->>REP: onInferenceServed(tokenId, user)
MP->>CR: recordCall(node, true, latencyMs)
MP->>CR: bumpOpenSessions(node, false)
MP->>SBT: checkAndAward(user, tokenId)
MP->>SBT: checkAndAward(ownerOf(tokenId), tokenId)
MP-->>-NODE: tx hash
NODE-->>-ORCH: ExecuteResponse
ORCH-->>-U: InferenceResponse
The compute layer is not simulated. backend/node/routes_node.py::execute loads a real HuggingFace transformers pipeline (inference.py::load_pipeline_from_metadata), runs the prompt through it, counts tokens via tokens.count_tokens, and signs settleInference from NODE_PRIVATE_KEY with a per-process nonce lock. Payment settlement is therefore a live on-chain transaction, not a synthetic event. Routes in routes_platform.py contain a seeded catalog and a _synth_nodes generator that drive the landing-page marquee and the /platform/nodes endpoint when the chain is empty — this is flagged in Known Limitations so nothing is hidden.
stateDiagram-v2
[*] --> Landing
Landing: app/page.tsx (marketing)
Landing --> Disconnected: enter app/*
state Disconnected {
[*] --> BrowseRO
BrowseRO: app/app/browse/page.tsx
BrowseRO --> ModelRO: click card
ModelRO: app/app/model/id/page.tsx
}
Disconnected --> Connected: WalletProvider.connect()
Connected: lib/wallet.tsx window.ethereum
state Connected {
[*] --> Dashboard
Dashboard: app/app/dashboard
Dashboard --> Browse: nav
Browse --> Model: select
Model --> SignSIWE: start inference
SignSIWE: WalletProvider.siwe() cached 240s
SignSIWE --> Paying: purchaseInference tx
Paying: contracts.ts Marketplace.purchaseInference
Paying --> Awaiting: receipt + sessionId
Awaiting --> Result: POST /api/infer
Result: settled on-chain + resultHash
Result --> Model
Model --> Upload: nav
Upload: POST /api/models/upload + ModelNFT.mintModel
Model --> NodesPage: nav
NodesPage: GET /api/nodes/probe
NodesPage --> RegisterNode: op path
RegisterNode: ComputeRegistry.registerNode
Dashboard --> Mine: nav
Mine: POST /api/mine/submit
Dashboard --> Profile
Profile: GET /api/platform/profile/address
}
Connected --> Disconnected: WalletProvider.disconnect()
| Route | Component | Description | Contract Calls |
|---|---|---|---|
/ |
app/page.tsx |
Marketing landing with Three.js globe and stats marquee | — |
/browse |
app/(app)/browse/page.tsx |
Model catalog with filters | — (reads via /api/platform/models) |
/model/[id] |
app/(app)/model/[id]/page.tsx |
Model detail + timeline + pricing | Marketplace.purchaseInference, Marketplace.subscribe |
/model/[id]/inference |
app/(app)/model/[id]/inference/page.tsx |
Prompt UI + on-chain settlement view | Marketplace.purchaseInference |
/upload |
app/(app)/upload/page.tsx |
Upload weights, mint NFT, list | ModelNFT.mintModel, Marketplace.list, Marketplace.setCollaborators |
/mine |
app/(app)/mine/page.tsx |
Proof-of-inference miner + owner earnings | Marketplace.withdraw, ModelNFT.withdraw |
/nodes |
app/(app)/nodes/page.tsx |
Registered compute nodes + probe | — |
/nodes/register |
app/(app)/nodes/register/page.tsx |
Stake and register as a node | ComputeRegistry.registerNode |
/nodes/me |
app/(app)/nodes/me/page.tsx |
Operator view of your node | ComputeRegistry.deregister |
/dashboard |
app/(app)/dashboard/page.tsx |
Per-address aggregates | — |
/profile/[address] |
app/(app)/profile/[address]/page.tsx |
Public profile | — |
/profile/me |
app/(app)/profile/me/page.tsx |
Your profile redirect | — |
/arena |
app/(app)/arena/page.tsx |
Side-by-side model battle | Marketplace.purchaseInference |
/compare |
app/(app)/compare/page.tsx |
Model comparison | — |
/architecture |
app/(app)/architecture/page.tsx |
In-app architecture diagram | — |
/flows |
app/(app)/flows/page.tsx |
React Flow tx diagrams | — |
/dna/[id] |
app/(app)/dna/[id]/page.tsx |
Neural-DNA fingerprint view | — |
/live |
app/(app)/live/page.tsx |
Live inference feed | — |
/run |
app/(app)/run/page.tsx |
Quick-run playground | Marketplace.purchaseInference |
/settings |
app/(app)/settings/page.tsx |
Wallet + limits | UserWallet.setLimits, UserWallet.deposit, UserWallet.withdraw |
/login |
app/(app)/login/page.tsx |
SIWE-only login | — |
| Layer | Technology | Version | Why This Choice |
|---|---|---|---|
| Contracts language | Solidity | 0.8.26 | Native transient-storage primitives used by ReentrancyGuardTransient |
| Contracts framework | Hardhat | 2.22.17 (packages/hardhat/package.json) |
hardhat-deploy replayable scripts + TypeChain v8 |
| Contract libs | OpenZeppelin Contracts | 5.1.0 | ERC-721/5192 + Ownable v5 APIs |
| Indexer | Ponder | see packages/ponder/package.json |
Typed GraphQL over multi-contract event set |
| Frontend framework | Next.js (App Router) | 15.0.3 | React 19 server components + edge routing |
| React | React | 19.0.0 | Concurrent features, use() in server components |
| Wallet client | viem | 2.21.45 | Typed publicClient / walletClient with chain definitions |
| SIWE (frontend) | siwe |
2.3.2 | Browser-side message builder |
| 3D | three + @react-three/fiber | 0.169.0 / 9.6.0 | Hero globe |
| UI animation | framer-motion | 11.11.1 | Section transitions |
| Icons | lucide-react | 0.454.0 | — |
| CSS | Tailwind CSS | 3.4.14 | — |
| Backend framework | FastAPI | 0.115.4 (backend/requirements.txt) |
Typed pydantic v2 models + async |
| ASGI server | uvicorn[standard] | 0.32.0 | — |
| Config | pydantic-settings | 2.6.1 | Typed env matrix |
| Ethereum client | web3.py | 7.5.0 | raw_transaction snake_case and EIP-1559 helpers |
| ETH account | eth-account | 0.13.4 | — |
| ML | transformers + tokenizers | 4.46.2 / 0.20.3 | HF pipelines used by node/inference.py |
| ML runtime | torch (CPU) | 2.6.0+cpu | From PyTorch CPU index |
| SIWE (backend) | siwe (Python) |
4.4.0 | — |
| Lit sidecar | @lit-protocol/lit-node-client, @lit-protocol/encryption |
Lit v7 (LIT_NETWORK.DatilDev) |
Custom-contract-call ACL |
| IPFS | Pinata v3 (uploads.pinata.cloud/v3/files) |
v3 | Free-tier 100 MB pins |
synergy-ggw/
├── app/ — Next.js 15 App Router
│ ├── layout.tsx — Root HTML + WalletProvider
│ ├── page.tsx — Marketing landing
│ ├── globals.css — Tailwind base
│ └── (app)/ — Authenticated app shell
│ ├── layout.tsx — Sidebar + TopBar
│ ├── architecture/ — In-app architecture diagram page
│ ├── arena/ — Side-by-side model battle
│ ├── browse/ — Model catalog
│ ├── compare/ — Model comparison
│ ├── dashboard/ — Per-address dashboard
│ ├── dna/[id]/ — Neural-DNA fingerprint
│ ├── flows/ — React Flow tx diagrams
│ ├── live/ — Live inference feed
│ ├── login/ — SIWE login
│ ├── mine/ — Owner earnings + miner
│ ├── model/[id]/ — Model detail + inference
│ ├── nodes/ — Registry, register, me
│ ├── profile/ — Public / me
│ ├── run/ — Quick-run playground
│ ├── settings/ — Wallet + limits
│ └── upload/ — Upload + mint
├── components/
│ ├── Navbar.tsx · HeroSection.tsx · GlobeSection.tsx · EarthGlobe.tsx
│ ├── StatsMarquee.tsx · AdvantagesSection.tsx · InnovationShowcase.tsx
│ ├── IntegrationsSection.tsx · DevelopmentSection.tsx · SecuritySection.tsx
│ ├── SolutionsCarousel.tsx · FinalCtaSection.tsx · Footer.tsx
│ ├── FeatureCard.tsx · FeatureSplitSection.tsx · FloatingCard.tsx
│ ├── CornerBrackets.tsx · IridescentArt.tsx · Logo.tsx
│ ├── GlobeSectionClient.tsx
│ ├── app-shell/ — Authenticated shell (Sidebar, TopBar, ModelActions, ...)
│ ├── arena/BattleArena.tsx
│ ├── dna/NeuralDNA.tsx
│ ├── flows/nodes.tsx
│ └── mine/ — Proof-of-inference UI (worker + dashboard)
├── lib/
│ ├── api.ts — Orchestrator client (/api/*)
│ ├── contracts.ts — viem writes to all 6 contracts
│ ├── wallet.tsx — WalletProvider + SIWE cache
│ ├── ponder.ts — GraphQL client for Ponder
│ ├── platform.ts — Typed /platform/* fetchers
│ ├── providerStore.ts — Compute provider local state
│ ├── mock.ts · simulate.ts — Demo-mode helpers
│ └── abis/ — Exported contract ABIs
├── backend/
│ ├── main.py — FastAPI entrypoint (ROLE switch)
│ ├── settings.py — Pydantic-settings env matrix
│ ├── requirements.txt · pyproject.toml · Dockerfile · docker-compose.yml
│ ├── .env.example · .env.node1.example · .env.node2.example
│ ├── orchestrator/
│ │ ├── routes_infer.py — POST /api/infer (rate-limited)
│ │ ├── routes_models.py — upload / version / download
│ │ ├── routes_platform.py — /platform/* aggregates
│ │ ├── routes_events.py — /platform/models/{id}/events
│ │ ├── routes_nodes.py — /api/nodes/probe
│ │ ├── routes_mining.py — /api/mine/{challenge,submit,leaderboard,me}
│ │ ├── ponder_client.py — GraphQL calls to Ponder
│ │ └── deps.py — SIWE + rate limit deps
│ ├── node/
│ │ ├── routes_node.py — /node/{execute,info,health}
│ │ ├── inference.py — HF pipeline loader
│ │ ├── model_cache.py — IPFS → HF cache materialisation
│ │ └── tokens.py — count_tokens
│ ├── shared/
│ │ ├── chain.py — web3.py + bootstrap_chain
│ │ ├── ipfs.py — Pinata v3 + gateway fallback
│ │ ├── lit_client.py — Lit sidecar HTTP client
│ │ ├── schemas.py — pydantic request/response models
│ │ └── siwe.py — SIWE verification
│ ├── abis/ — Mirrored ABIs consumed by web3.py
│ └── tests/ — pytest suite
├── packages/
│ ├── hardhat/ — Solidity + Hardhat
│ │ ├── contracts/ — ModelNFT, Marketplace, ComputeRegistry, UserWallet, Reputation, BadgeSBT + interfaces
│ │ ├── deploy/ — 00_ModelNFT → 04_Wiring (hardhat-deploy)
│ │ ├── hardhat.config.ts
│ │ ├── test/ — Hardhat tests
│ │ └── deployments/ — hardhat-deploy artefacts
│ ├── ponder/ — Ponder v7 indexer (src/index.ts)
│ └── shared/abis/ — Shared ABI export
├── services/
│ └── lit-agent/ — Lit sidecar (Express + Lit v7)
│ └── src/server.ts — /encrypt · /decrypt · /health
├── integrations/
│ └── langchain-synapse/ — LangChain LLM wrapper (PyPI: synapse-langchain)
├── scripts/
│ ├── dev-up.ps1 · dev-up.sh — local orchestration
│ ├── sync-env.ts — writes addresses into every .env after deploy
│ ├── fund-nodes.ts — funds NODE_*_ADDRESS with Sepolia ETH
│ ├── register-nodes.ts — calls ComputeRegistry.registerNode
│ ├── seed.ts — mints demo models + subscriptions
│ ├── reclaim-stuck-sessions.ts — calls Marketplace.reclaimExpiredSession
│ └── e2e-one.ts · e2e_real_upload.py — end-to-end smoke tests
├── next.config.mjs · tailwind.config.ts · postcss.config.mjs
├── tsconfig.json
├── package.json — root workspace
├── CHANGES.md · HANDOVER.md · HANDOVER_Backend.md
├── HANDOVER_MultiLaptop.md · HANDOVER_ProviderNetwork.md
├── INFRA_INTEGRATION.md · TESTER_PROMPT.md
└── README.md
- Node.js ≥ 20 — for Next.js 15 and Hardhat 2.22.
- Python 3.11+ — pinned package set in
backend/requirements.txt(FastAPI 0.115, web3.py 7.5, transformers 4.46, torch 2.6.0+cpu). - MetaMask or any EIP-1193 wallet — Sepolia funded.
- Pinata v3 JWT — or run in dev-mock mode (see below).
- Alchemy Sepolia RPC — any Sepolia RPC that supports
eth_getLogsfor reads; writes go through MetaMask. - Lit operator key — for
services/lit-agent, an EOA on Chronicle Yellowstone that is also registered onComputeRegistrywith an empty task list.
git clone <repo-url> synapse && cd synapse
# Root (Next.js + workspaces)
npm install
# Backend (Python)
python -m venv .venv
.venv\Scripts\activate # PowerShell (Windows)
pip install -r backend/requirements.txt
# Lit sidecar
npm install --workspace @synapse/lit-agentEvery variable below lives in one of the committed .env.example files or is consumed by source code directly.
| Variable | Description | Example | Required |
|---|---|---|---|
ROLE |
Backend role switch (orchestrator | node) |
orchestrator |
yes (backend) |
PORT |
FastAPI port | 8000 |
no |
FRONTEND_URL |
CORS allow-origin | http://localhost:3000 |
yes |
VERSION |
Advertised in /health |
0.5.0 |
no |
ALCHEMY_SEPOLIA_RPC |
Sepolia RPC for writes + non-log reads | https://eth-sepolia.g.alchemy.com/v2/... |
yes |
CHAIN_ID |
EVM chain id | 11155111 |
no |
MODEL_NFT_ADDRESS |
Deployed ModelNFT |
0x61Ef...1132 |
yes |
MARKETPLACE_ADDRESS |
Deployed Marketplace |
0xd34d...9d75 |
yes |
USER_WALLET_ADDRESS |
Deployed UserWallet |
0x6a23...127b |
yes |
COMPUTE_REGISTRY_ADDRESS |
Deployed ComputeRegistry |
0x9D24...470d |
yes |
REPUTATION_ADDRESS |
Deployed Reputation |
0xf602...D52E |
yes |
BADGE_SBT_ADDRESS |
Deployed BadgeSBT |
0xE786...5d93 |
yes |
CONTRACTS_DEPLOY_BLOCK |
fromBlock for eth_getLogs |
10686153 |
no |
EVENTS_RPC_URL |
Wide-range log RPC | https://ethereum-sepolia-rpc.publicnode.com |
no |
PINATA_JWT |
Pinata v3 bearer | eyJhbGciOi... |
yes (or use dev mock) |
IPFS_GATEWAY_URLS |
CSV fallback list | https://gateway.pinata.cloud/ipfs/,https://dweb.link/ipfs/,https://ipfs.io/ipfs/ |
no |
LIT_SIDECAR |
URL of services/lit-agent |
http://localhost:7777 |
yes |
LIT_KILL_SWITCH |
Bypass encryption end-to-end | 0 |
no |
HF_HUB_CACHE |
Local HF cache dir (node) | ./.hf-cache |
no |
SIWE_EXPECTED_DOMAIN |
Must equal window.location.host at signing time |
localhost:3000 |
yes |
SIWE_MAX_AGE_SECONDS |
SIWE message age cap | 300 |
no |
INFER_RATE_PER_MIN |
Rate limit on /api/infer |
30 |
no |
PONDER_URL |
Ponder GraphQL | http://localhost:42069 |
no |
DEV_MOCK_IPFS |
Bypass Pinata → disk (.ipfs-mock/) |
false |
no |
DEV_MOCK_IPFS_DIR |
Mock dir | ./.ipfs-mock |
no |
NODE_PRIVATE_KEY |
EOA used by compute node to sign settleInference |
0xabc… |
yes (node) |
NODE_ENDPOINT |
HTTPS URL advertised on-chain | https://node-1.example.com |
yes (node) |
NODE_TASKS |
CSV of pipeline tags served | text-classification,summarization,text-generation,question-answering |
no |
NODE_KILL_SWITCH |
Node returns 503 on /node/execute |
false |
no |
DEPLOYER_PRIVATE_KEY |
Hardhat deployer (packages/hardhat/.env) |
0xdef… |
yes (deploy) |
ETHERSCAN_API_KEY |
For verify:sepolia |
ABCD... |
no |
BUYER_PRIVATE_KEY |
Used by scripts/seed.ts |
0x... |
no (demo) |
NODE_1_ADDRESS / NODE_1_PRIVATE_KEY / NODE_1_ENDPOINT |
Seeded node 1 | — | no (demo) |
NODE_2_ADDRESS / NODE_2_PRIVATE_KEY / NODE_2_ENDPOINT |
Seeded node 2 | — | no (demo) |
LIT_OPERATOR_ADDRESS / LIT_OPERATOR_PRIVATE_KEY |
Operator signer for services/lit-agent |
— | yes (lit) |
ORCHESTRATOR_URL |
For seed.ts smoke calls |
http://localhost:8000 |
no |
NEXT_PUBLIC_ORCHESTRATOR_URL |
Frontend → backend | http://localhost:8000 |
no |
NEXT_PUBLIC_PONDER_URL |
Frontend → Ponder | http://localhost:42069 |
no |
NEXT_PUBLIC_SEPOLIA_RPC |
Frontend RPC | https://eth-sepolia.g.alchemy.com/v2/... |
no (default hardcoded) |
NEXT_PUBLIC_SIWE_DOMAIN |
Domain used in SIWE message | localhost:3000 |
no |
NEXT_PUBLIC_SIWE_URI |
URI used in SIWE message | http://localhost:3000 |
no |
NEXT_PUBLIC_MODEL_NFT_ADDRESS |
Frontend ModelNFT |
0x61Ef… |
no (default hardcoded) |
NEXT_PUBLIC_MARKETPLACE_ADDRESS |
Frontend Marketplace |
0xd34d… |
no (default hardcoded) |
NEXT_PUBLIC_COMPUTE_REGISTRY_ADDRESS |
Frontend ComputeRegistry |
0x9D24… |
no (default hardcoded) |
NEXT_PUBLIC_USER_WALLET_ADDRESS |
Frontend UserWallet |
0x6a23… |
no (default hardcoded) |
NEXT_PUBLIC_REPUTATION_ADDRESS |
Frontend Reputation |
0xf602… |
no (default hardcoded) |
NEXT_PUBLIC_BADGE_SBT_ADDRESS |
Frontend BadgeSBT |
0xE786… |
no (default hardcoded) |
WARNING: The six Sepolia contract addresses are currently hardcoded in
lib/contracts.tsat lines 48–59. They must be moved to environment configuration before production deployment.WARNING: A default Alchemy Sepolia RPC URL is hardcoded in
lib/wallet.tsxline 70 andlib/contracts.tsline 76. This key must be rotated and moved out of source before production deployment.
# (packages/hardhat/.env) — fill DEPLOYER_PRIVATE_KEY, ETHERSCAN_API_KEY, SEPOLIA_RPC_URL
npm run chain:deploy:sepolia # hardhat deploy --network sepolia + sync-envnpm run chain:deploy:sepolia (root package.json) runs hardhat deploy against Sepolia, executes the 5-step deploy in packages/hardhat/deploy/, and then runs scripts/sync-env.ts which writes the new addresses into backend/.env, packages/ponder/.env, and packages/nextjs/.env.local.
To fund and register compute nodes afterwards:
npm run fund-nodes # scripts/fund-nodes.ts
npm run register-nodes # scripts/register-nodes.ts
npm run seed # scripts/seed.ts (mints demo models)# orchestrator
.venv\Scripts\activate
ROLE=orchestrator PORT=8000 uvicorn backend.main:app --reload --port 8000
# node 1 (separate shell)
uvicorn backend.main:app --port 3001 --env-file backend/.env.node1
# node 2 (separate shell)
uvicorn backend.main:app --port 3002 --env-file backend/.env.node2
# lit sidecar (separate shell)
npm run lit:dev
# ponder (separate shell)
npm run ponder:devOn Windows there is a one-shot PowerShell harness:
.\scripts\dev-up.ps1npm run dev # next dev -p 3000Open http://localhost:3000.
All orchestrator routes are mounted under /api. Node routes are mounted under /node on the node process.
- Description: Common health for both roles. Returns role + port + version.
- Auth: not required.
{
"status": "string — always 'ok'",
"role": "string — 'orchestrator' | 'node'",
"port": "number — listening port",
"version": "string — settings.version"
}- Description: Orchestrator-only health.
- Auth: not required.
{ "status": "string", "version": "string" }- Description: Multipart upload: pins weights + metadata, optionally encrypts via Lit, returns CIDs plus
predictedTokenIdthat the caller must mint. - Auth: not required (caller signs the mint tx themselves).
- Request (multipart/form-data):
{
"file": "binary — model weights (<=100MB) or empty when synapse.hf_hub_id is set",
"metadata_json": "string — JSON blob per SCHEMAS §1",
"creator": "string — 0x-address minting the NFT",
"marketplace_address": "string — Marketplace contract",
"modelnft_address": "string — ModelNFT contract",
"is_tar_archive": "boolean — whether 'file' is a tar bundle",
"encrypt": "boolean — default true; respected unless LIT_KILL_SWITCH"
}- Response:
{
"modelCID": "string — IPFS CID of the (encrypted) weights blob",
"metadataCID": "string — IPFS CID of the metadata JSON",
"predictedTokenId": "string — totalSupply()+1",
"encrypted": "boolean",
"litDataHash": "string | null",
"litAccessControlConditionsCid": "string | null — CID of the pinned ACL"
}- Description: Owner-signed version bump. Frontend still submits
ModelNFT.addVersion(...)afterwards. - Auth:
Authorization: SIWE <base64(message)>.<signature>; SIWE signer must equalModelNFT.ownerOf(token_id). - Request: same shape as
/models/uploadpluschangelog. - Response:
{
"modelCID": "string",
"metadataCID": "string",
"tokenId": "string",
"versionIndexHint": "number — authoritative value comes from VersionAdded event",
"encrypted": "boolean",
"litDataHash": "string | null"
}- Description: Streams decrypted weights to the authenticated caller. Caller must be the NFT owner or an active subscriber (enforced via
Marketplace.hasAccess). - Auth: SIWE required.
- Response:
application/octet-streamorapplication/x-tarchunked stream.
- Description: Validates the on-chain session, forwards the prompt to the assigned node, returns the settled response. Rate-limited per IP by
INFER_RATE_PER_MIN. - Auth: not required to match the schema; backend cross-checks
useragainstMarketplace.sessions(sessionId).user. - Request:
{
"sessionId": "string — 0x-prefixed 32-byte session id",
"tokenId": "string | number — must match on-chain session.tokenId",
"user": "string — 0x-address",
"prompt": "string",
"maxTokens": "number — 1..4096, default 128",
"parameters": "object — pipeline kwargs (optional)"
}- Response:
{
"output": "string",
"outputStr": "string",
"inputTokens": "number",
"outputTokens": "number",
"latencyMs": "number",
"settlementTxHash": "string",
"resultHash": "string",
"nodeAddress": "string",
"verified": "boolean | null"
}- Description: Platform-wide counters. Ponder first; seeded synthetic fallback keeps the UI alive.
- Response:
{
"models": "number",
"onchainModels": "number",
"inferences": "number",
"inferencesPerMinute": "number",
"nodes": "number",
"onlineNodes": "number",
"onchainNodes": "number",
"totalEarnedEth": "number",
"uptimeSeconds": "number",
"chainId": "number"
}- Description: Paginated model catalog. Supports
pipeline,license,owner,q,sort=popular|newest|rated|cheapest,limit.
- Description: Per-model detail. 3-layer fallback: Ponder → chain-direct
ModelNFT.model(id)+Marketplace.pricing(id)+ IPFS metadata → seeded catalog.
- Description: Live on-chain event timeline from
eth_getLogsoverModelNFT + Marketplace, filtered bytokenId. 15 s in-process TTL cache.
- Description: Merged node list: Ponder + direct
ComputeRegistry+ seeded synthetic.
- Description: Most recent settled inferences. Params:
limit,modelId,address.
- Description: Public aggregates for an address. Includes activity heatmap + earned SBT badges.
- Description: Connected-user dashboard: owned models, running subscriptions, open sessions, SBTs, miner credits.
- Description: Per-operator node view: stake, earnings, jobs served.
- Description: Live reachability probe: walks
ComputeRegistry.nodeList, GETs<endpoint>/healthwith a 3 s deadline, and returns a merged view with cache TTL 20 s.
- Description: Returns the current mining challenge + difficulty + reward formula. Rotates every ~10 s.
- Description: Submits a share. Request:
{ address, challenge, nonce, difficulty, hashesInTick }. Server verifiesmix32(fnv1a(challenge) ^ nonce) <= (0xffffffff >> difficulty), dedupes by(challenge,nonce,difficulty), credits2^(difficulty-16) * 10to the address.
- Description: Top miners by credits.
- Description: Per-address miner stats.
- Description: Re-validates the session, ensures the model is materialised locally, runs the HF pipeline, signs
settleInference, returns the response. - Request:
{
"sessionId": "string",
"tokenId": "number | string",
"prompt": "string",
"maxTokens": "number",
"metadataCID": "string",
"pipelineTag": "string",
"parameters": "object (optional)"
}- Response: see
/api/inferresponse (identical schema; the orchestrator passes it through).
- Description: Node self-report: EOA address, advertised endpoint, tasks, currently-loaded models.
- Description: 200 if the node is alive. Returns 503 when
NODE_KILL_SWITCH=1.
- Hardcoded Sepolia contract addresses and RPC key in
lib/contracts.tsandlib/wallet.tsx— easy unblock for judging, must move to env before mainnet. - Seeded catalog in
backend/orchestrator/routes_platform.py—_build_catalog,_synth_nodes,_PROMPTS,_OUTPUTS, and_activity_heatmapare deterministic pseudo-data used as a fallback for/platform/stats,/platform/models,/platform/nodes, and/platform/inferences/recentwhen Ponder and the contracts return nothing. The real on-chain paths always win when data exists; the synthetic rows are flagged with"onchain": false. - Dev mock IPFS — when
PINATA_JWTis missing orDEV_MOCK_IPFS=1,backend/shared/ipfs.pywrites pins to./.ipfs-mock/with a deterministic fake CID prefixbafkmock…. Lets the stack run without a Pinata account but breaks inter-machine resolution. - Lit kill-switch —
LIT_KILL_SWITCH=1(set in both backend andservices/lit-agent) makes upload and download skip Lit entirely and pass ciphertext through as plaintext base64. Useful when the Litdatil-devnetwork flakes, unsafe for real users. - Mining pool in a JSON file —
backend/.state/mining-pool.jsonis sufficient for a single orchestrator; multi-orchestrator deployment requires Postgres. versionIndexHintin/models/{id}/version— server-side best guess; authoritative value comes from theVersionAddedevent. Fixed once the frontend reads the event directly.- No slashing on
reclaimExpiredSession— the session refunds the user and decrements the node's success counters, but does not slash the stake. Slashing is wired in the contract interface but not yet enforced at the value level. torch==2.6.0+cpuonly — GPU acceleration is not configured. Works for small HF models; will be painful on 7B+ ones.- Dead-node dispatch — if
Marketplace.selectNodepicks a node whose uvicorn is down,/api/inferreturns HTTP 502 with a clear message and the user's escrow unlocks afterSESSION_TIMEOUTviaMarketplace.reclaimExpiredSession. No active liveness gating at contract level yet. services/lit-agent500 MB request body limit — streaming encrypt/decrypt is not wired, capping the on-chain model size to what fits in one Node.jsexpress.jsoncall.packages/nextjs/.env.localpath inscripts/sync-env.ts— the sync script still writes to the legacypackages/nextjs/path; the app at the repo root reads.env.localdirectly. You may need to copy manually until the script is updated.
- Move all hardcoded addresses and RPC URLs out of source.
- Implement contract-level slashing on session expiry + a proper dispute-resolution window.
- GPU-enabled node image (
torch==2.6.0+cu124) with CUDA-awaremodel_cache.py. - Postgres-backed mining pool and SIWE nonce store so the orchestrator can scale horizontally.
- Stream-friendly encryption path so
services/lit-agentdoes not need to buffer the whole model in memory. - TEE / attestation hooks so
InferenceSettled.resultHashcan be backed by a remote attestation from the node. - Replace the seeded catalog with an
indexingFromBlockdisclaimer once Ponder is guaranteed to be running alongside the orchestrator.
- Re-entrancy.
Marketplace.solandModelNFT.solinheritReentrancyGuardTransientfrom OpenZeppelin v5 and the payout path uses pull-payment viapendingWithdrawals[...] + withdraw(). - Fee rate limiting.
Marketplace.setFeesenforcesprotocolFeeBps + nodeFeeBps <= 3000and caps step changes at 100 bps up — prevents a compromised owner from draining listings in one tx. - Pull-payment. Every payout (
_distribute, expired-session refund, Withdrawable) accrues topendingWithdrawalsand is withdrawn by the beneficiary. No automatictransferto arbitrary addresses on the hot path. - Session ownership.
/api/inferand/node/executere-readMarketplace.sessions(sessionId)on-chain and reject user mismatch (403), tokenId mismatch (400), settled (409), node mismatch (403 at node layer). - SIWE.
lib/wallet.tsxcaches tokens for 240 s (<300 s backend max).backend/orchestrator/deps.pypins the expected domain toSIWE_EXPECTED_DOMAIN— no silent placeholder. Messages are base64'd in theAuthorization: SIWE <msg>.<sig>header. - CORS.
main.pyuses explicitallow_origins=[settings.frontend_url]withallow_credentials=True. Never*+ credentials (which the browser rejects anyway). - Upload limits.
routes_models.upload_modelcaps at 100 MB (Pinata free-tier). - Rate limit.
INFER_RATE_PER_MIN=30on/api/infer, enforced per IP bydeps.rate_limit_infer. - Mining pool. Nonce submissions are server-verified (
_verify_share), deduped per(challenge, nonce, difficulty), and GC'd after 2 h.SubmitBodyvalidates difficulty[12..28]and nonce[0..2^32). - Lit ACL. Every encrypt bakes the predicted
tokenIdinto the evmContractConditions ACL; the sidecar rejectstokenId === "0"at/encrypt(services/lit-agent/src/server.ts). - Kill switches.
LIT_KILL_SWITCH(both sides) andNODE_KILL_SWITCHlet operators take components out of circulation cleanly. - Node kill-switch propagation.
/node/executereturns 503 when flipped, so the orchestrator surfaces a 502 andreclaimExpiredSessioncan refund. - Tx signing. Nodes sign
settleInferencewith their ownNODE_PRIVATE_KEYunder an asyncio_tx_lockto serialize nonces (backend/node/routes_node.py). One signer, one nonce stream. - Global error handler.
main.pyinstalls a catch-all handler that returns{"detail": "internal server error", "type": "..."}— no raw tracebacks leak to clients.
- Fork the repo and create a feature branch (
feat/<slug>). - Run
npm installat the root andpip install -r backend/requirements.txtinside a Python 3.11 venv. - Keep changes scoped. Code style:
- TypeScript. App Router + server components, strict mode, 2-space indent. Use viem for chain calls — do not introduce ethers.js to the frontend.
- Solidity. 0.8.26, OpenZeppelin v5 imports only. Every
externalthat mutates state must be protected bynonReentrantor documented as safe. Update the interface incontracts/interfaces/alongside the implementation. - Python. FastAPI + pydantic v2, type hints everywhere,
anyio.to_thread.run_syncto wrap web3.py blocking calls. Put env inbackend/settings.py; do not scatteros.getenvacross routes.
- Tests:
- Contracts:
npm run --workspace @synapse/hardhat test(ortest:gas). - Backend:
pytest backend/tests. - Frontend: type-check via
tsc --noEmit(no e2e harness yet).
- Contracts:
- For contract changes, re-export ABIs (
npm run --workspace @synapse/hardhat export-abis:sepolia) and re-runnpm run sync-env. Commit both the contract change and the synced artefacts. - Do not commit
.envfiles. The gitignored paths arebackend/.env*,packages/hardhat/.env,packages/ponder/.env,services/lit-agent/.env, and.env.localat the repo root. - Open a PR against
mainwith a short changelog. Reference the contract address inpackages/hardhat/deployments/sepolia/when the change touches on-chain state.
- darved2305 — Darshan Ved
- HetanshWaghela — Hetansh Waghela
- N1KH1LT0X1N — Nikhil Pravin Pise
- VrajVed — HotPlate
MIT. See LICENSE.
- OpenZeppelin Contracts v5 — ERC-721, ERC-5192, Ownable, ReentrancyGuardTransient.
- Lit Protocol v7 —
datil-devnetwork,encryptString/decryptToString,evmContractConditionscustom-contract-call ACL. - Ponder — multi-contract event indexer with typed GraphQL.
- Pinata v3 — IPFS pinning service.
- HuggingFace
transformers— pipeline runtime on every compute node. - viem — typed wallet + public clients for the frontend.
- web3.py 7.5 — backend contract calls.
- ethereum-sepolia-rpc.publicnode.com — wide-range
eth_getLogsthat Alchemy's free tier does not serve.
- NFT — Non-Fungible Token, ERC-721 here. Each
ModelNFTtokenId is a unique model identity. - SBT — Soulbound Token (ERC-5192); a non-transferable NFT used for achievement badges.
- SIWE — Sign-In With Ethereum (EIP-4361); an ERC-4361 message the user signs in MetaMask so the backend can authenticate them without a password.
- ACL — Access Control List. In Lit Protocol v7 it is the
evmContractConditionsobject that tells the Lit network which on-chain conditions a caller must satisfy to decrypt. - ERC-5192 — "Minimal Soulbound NFTs"; adds
locked(tokenId)and aLocked(tokenId)event to mark a token as non-transferable. - IPFS CID — Content Identifier; a self-describing multihash that addresses a blob on IPFS.
- Session — the
Marketplace.Sessionstruct opened bypurchaseInferenceand closed bysettleInferenceorreclaimExpiredSession. - Pipeline tag — HuggingFace task identifier (e.g.
text-generation,summarization). Drives node selection viaComputeRegistry.selectNode. - EWMA — Exponentially Weighted Moving Average; used by
ComputeRegistry.recordCallto track node latency. - Pull payment — pattern where the payee calls
withdraw()to claim funds, rather than the contractsend()-ing to them, avoiding re-entrancy and the 2300-gas stipend trap. - Compute node — a machine running
ROLE=nodethat stakes on-chain, serves HuggingFace pipelines, and signssettleInferencefrom its own key. - Kill switch — a boolean env var (
LIT_KILL_SWITCH,NODE_KILL_SWITCH) that takes a component out of rotation without stopping the process.