Base mainnet · SessionRouter (Inference Contract)

Staked MOR session lifecycle

For your own consumer node: open escrow, close, unused back now, used locked until the next UTC day. On-chain protocol staking — not the API Gateway credit model.

End-to-end flow

Session lifecycle: wallet to open to active to close; unused returns to wallet now; used is day-locked until next UTC day then claimable.

Walkthrough

1. MOR starts in your wallet.

2. You open a session. That MOR is escrowed in the Inference Contract for the session length (set from stake and price).

3. While open, the session is active — stake is reserved for inference.

4. Someone submits close (your node after the end time, or you via API). Timing does not change the rules below — early, on-time, or a bit late is still just close.

5–6. On close, stake splits two ways:

  • Unused — returned to your wallet in that same close transaction. Ran the whole session? Unused is ~0 (nothing left over).
  • Used — day-locked in the contract until the next UTC day. Ran the whole session? That’s essentially all of your stake, locked until then.

7–8. After the lock lifts, claim the used slice (app withdraw flow, or withdrawUserStakes). It does not auto-return. Plan float for gross daily stake — used MOR cannot open another session the same UTC day.

The clock hitting zero does not close by itself; a close transaction still has to land. If it never does, stake stays active until it does. Funding / gas issues 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 (used-stipend day-lock 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”

Close is one blockchain transaction. It needs gas and a solvent protocol funding wallet (separate from yours) to pay the provider. If funding or approval is short, the whole close fails and your stake stays in an active session until a close succeeds.

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] — the used slice after close, held until releaseAt = startOfTheDay(min(closedAt, endsAt)) + 1 day. getUserStakesOnHold returns locked vs claimable; withdrawUserStakes only pays out past releaseAt.

What closeSession does in one transaction

  1. Sets closedAt; session inactive.
  2. _rewardUserAfterClose: sessionEnd = min(closedAt, endsAt). Unused → wallet. Used → on-hold if still before releaseAt.
  3. _rewardProviderAfterClose pays the provider (typically from protocol fundingAccount, not your wallet).

Bottom line: Close always splits unused / used the same way. Check getUserStakesOnHold, then withdrawUserStakes when releasable. Still missing? Look for an unclosed session (closedAt == 0) or a failed close (funding / gas).