Base mainnet · SessionRouter (Inference Contract)

Staked MOR session lifecycle

For running your own consumer node: how stake moves through the Inference Contract (SessionRouter), when MOR comes back to your wallet, and why funding liquidity matters for closing sessions. This is on-chain Morpheus protocol staking — not the API Gateway path, which uses a different staking, credit, and recovery model.

End-to-end flow

flowchart TB n1(["1 · MOR in your wallet"]):::cGreen n2(["2 · Approve + Open"]):::cBlue n3(["3 · Active Session"]):::cPurple n7(["7 · MOR in your wallet"]):::cGreen n1 --> n2 --> n3 subgraph rowA["Path A: Natural Session Expiration"] direction LR n4a(["4a · C-node closes ~1m"]):::cOrange n5a(["5a · No TimeLock"]):::cYellow n6a(["6a · Refund in Close txn"]):::cYellow n4a --> n5a --> n6a end subgraph rowB["Path B: Early Session Close"] direction LR n4b(["4b · User Calls Close via API"]):::cBlue n5b(["5b · Timelock in Inference Contract"]):::cYellow n6b(["6b · Claim when ready"]):::cBlue n4b --> n5b --> n6b end n3 --> n4a n3 --> n4b n6a --> n7 n6b --> n7 classDef cGreen fill:#052e16,stroke:#22c55e,stroke-width:2px,color:#d1fae5 classDef cBlue fill:#172554,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe classDef cPurple fill:#3b0764,stroke:#a855f7,stroke-width:2px,color:#e9d5ff classDef cOrange fill:#431407,stroke:#ea580c,stroke-width:2px,color:#fed7aa classDef cYellow fill:#422006,stroke:#ca8a04,stroke-width:2px,color:#fef08a

Walkthrough

1. You start with MOR in your own wallet, like any normal token balance.

2. You approve the app and Open a session. Your MOR moves into the shared contract (the Inference Contract) for the duration of the session, and a scheduled end time is chosen from the price and how much you staked.

3. Until that end time, the session is active: your stake is reserved for inference with the provider.

4. The session is closed with one blockchain transaction. The path splits depending on whether time ran out first or you asked to close early.

  • a. Natural Session Expiration: After the scheduled end, your consumer node usually submits the closing transaction for you (often within about a minute, once it is online and caught up).
  • b. Early Session Close: Before the scheduled end, you request a close through the API (or app). That is the first user-initiated step on this path.

5. Token state after closing — what happens to your MOR in the contract right when the closing transaction succeeds (the yellow boxes in the diagram).

  • a. Natural expiration: There is no timelock on your refund for this session. Nothing extra is parked for you to claim later from this close.
  • b. Early close (the tricky part): The contract tries to pay the provider for the time you actually used. Because you stopped early, it may hold back part of your stake inside the Inference Contract for a set waiting period instead of sending it all to your wallet at once. How much depends on how long the session had been running and the agreed price — it is not a simple “minutes left on the clock” slider. Why it is held: to mirror the same kind of settlement rules the contract uses when sessions end on schedule, so early exits are not cheaper in a way that breaks the economics. When you get the rest: after that waiting period ends, step 6b is when you (or your app) run the separate claim / withdraw stake action so the held amount can move to your wallet. Until then, that slice stays inside the contract even though the session is already “closed.”

6. Last mile back to your wallet.

  • a. Natural expiration: Your refund for this session is sent inside the same closing transaction. You do not need a separate “withdraw” for this session’s refund.
  • b. Early close: When: You can claim once the contract’s unlock time has passed. In practice that is tied to the UTC calendar day when you closed: think “after the end of the next full UTC day” from that close (your app or a block explorer can show the exact timestamp). How: Use the withdraw stake / claim flow in your consumer app, or interact with the same Morpheus Inference Contract on Base that held your session — that is where the timelocked balance lives until you pull it to your wallet. This is the second user-initiated step on the early-close path.

7. You are done when spendable MOR is back in your wallet: right after a successful natural-expiration close, or after an early close plus the claim step once the timelock allows it.

The contract does not close the session by itself when the clock hits zero; something still has to submit that closing transaction. If that never happens, your stake stays in the active session until it does. Funding, network, and node issues are covered below.

Check a consumer wallet (read-only)

Enter your consumer address to see an on-chain snapshot on Base: the same three places as the diagram — in your wallet, active in the Inference Contract (open sessions), and on hold in the Inference Contract (early-close timelock queue). Uses a small sample of your newest sessions so the page stays fast; use Scanner technical details for the full breakdown. If the default RPC fails in your browser, paste another Base HTTPS endpoint.

When closes fail or balances look “stuck”

Closing a session is one blockchain transaction. For it to succeed, the network has to accept it and the protocol has to be able to pay the provider from a separate protocol funding wallet (not your consumer wallet). If that funding side is short on MOR or has not granted the contract enough permission, the whole close fails — whether you closed naturally or early. Your stake then stays in an active session until a close finally goes through.

Consumer node API & curl

Paths match proxy-router (Morpheus consumer node). Default API port is often 8082 (inference may use 3333 — use the HTTP admin/API port). Replace placeholders before running. Auth is HTTP Basic (-u 'user:password'); your node operator configures users and per-method allowlists — omit -u only if auth is disabled (e.g. some local setups). See also /swagger/index.html on your node.

Ready to claim on-hold MOR? Start with section 1 below — withdrawUserStakes on the Inference Contract (no HTTP shortcut on the node today).

1. Recover held MOR — withdrawUserStakes (on-chain write)

There is no withdrawUserStakes route on the consumer node HTTP API in current proxy-router; you send a transaction to the Inference Contract on Base. The caller must be the delegatee allowed for that consumer in session rules (usually the same key your consumer node uses). iterations_ caps how many releasable on-hold rows to process (e.g. 20).

cast send 0x6aBE1d282f72B474E54527D93b979A4f64d3030a \
  "withdrawUserStakes(address,uint8)" 0xYOUR_CONSUMER_WALLET 20 \
  --rpc-url https://mainnet.base.org \
  --private-key "$PRIVATE_KEY_OF_DELEGATEE"

Function selector (for custom tooling): 0xa98a7c6b. Use a hardware wallet / MetaMask “Interact with contract” with the same ABI if you do not use cast.

2. List sessions for a consumer wallet (read chain via node)

Returns session structs from the node’s RPC. Query params: user (consumer address), offset, limit (1–255), order (asc or desc).

curl -sS -u 'YOUR_API_USER:YOUR_API_PASSWORD' \
  'https://YOUR_CNODE_HOST:8082/blockchain/sessions/user?user=0xYOUR_CONSUMER_WALLET&offset=0&limit=20&order=desc'

3. List session IDs only (lighter)

Same query params as above; response is ID list only.

curl -sS -u 'YOUR_API_USER:YOUR_API_PASSWORD' \
  'https://YOUR_CNODE_HOST:8082/blockchain/sessions/user/ids?user=0xYOUR_CONSUMER_WALLET&offset=0&limit=20&order=desc'

4. Fetch one session by ID (read)

curl -sS -u 'YOUR_API_USER:YOUR_API_PASSWORD' \
  'https://YOUR_CNODE_HOST:8082/blockchain/sessions/0xYOUR_SESSION_ID'

5. Close a session (write — node submits closeSession)

POST with empty body. The node builds the provider receipt and broadcasts the tx. Requires API permission close_session. Session id is 0x + 64 hex chars.

curl -sS -X POST -u 'YOUR_API_USER:YOUR_API_PASSWORD' \
  -H 'Accept: application/json' \
  'https://YOUR_CNODE_HOST:8082/blockchain/sessions/0xYOUR_SESSION_ID/close'

6. Read on-hold balances (direct Base RPC — no node)

Inference Contract on Base mainnet: 0x6aBE1d282f72B474E54527D93b979A4f64d3030a. Encode calldata with Foundry cast (or any ABI encoder). iterations_ is a hint for internal reads; 1 is typical.

DATA=$(cast calldata "getUserStakesOnHold(address,uint8)" 0xYOUR_CONSUMER_WALLET 1)

curl -sS https://mainnet.base.org \
  -H 'Content-Type: application/json' \
  -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_call\",\"params\":[{\"to\":\"0x6aBE1d282f72B474E54527D93b979A4f64d3030a\",\"data\":\"$DATA\"},\"latest\"]}"

Where is my MOR? (straight from the code)

Your consumer MOR can only be in three on-chain places. The Morpheus consumer node (proxy-router) does not custody tokens; it calls the same Inference Contract functions you could call with another tool. It runs rehydrateFromChain on startup and a 1-minute loop to submit closeSession for expired, unclosed sessions it tracks.

1. Your wallet

Normal ERC-20 balanceOf(you) on the MOR token. Anything the contract safeTransfers to you during closeSession or withdrawUserStakes lands here.

2. Inside Inference Contract, open session

openSession does transferFrom(you, InferenceContract, amount) (SessionRouter.sol). While closedAt == 0, that stake lives in the session record — it is not in your wallet and not in userStakesOnHold yet. Block explorers still show the tokens on the Inference Contract balance.

3. Inside Inference Contract, on-hold queue

Array userStakesOnHold[user]. Entries are only created on certain early closes (closedAt < endsAt) in _rewardUserAfterClose, with releaseAt = startOfTheDay(closedAt) + 1 days. getUserStakesOnHold splits amounts into hold_ (before releaseAt) vs available_ (after). withdrawUserStakes only moves past-releaseAt rows to your wallet.

What closeSession does in one transaction

  1. Sets closedAt and marks the session inactive.
  2. _rewardUserAfterClose: if closedAt ≥ endsAt, no new on-hold row; your share (session.stake minus the direct-payment carve-out if any) is safeTransfer’d to you here. If closedAt < endsAt, a computed slice may be pushed to userStakesOnHold and the rest safeTransfer’d.
  3. _rewardProviderAfterClose pays the provider (for typical sessions, from the protocol fundingAccount via transferFrom, not from your wallet in that same step).

Bottom line: If you closed on or after endsAt, your session stake (for standard staked flows) is not waiting in userStakesOnHold — it was sent to your wallet inside closeSession. If you closed early, check getUserStakesOnHold for locked vs available, then call withdrawUserStakes when there is something releasable. If tokens are still “missing,” look for an unclosed session (closedAt == 0) or a failed close (funding / gas).