Skip to content

RPC reference ​

A Scalar Commons node serves 110 JSON-RPC methods over both HTTP and WebSocket on the same port (9944 on the reference devnet). That is the count on a default, pruning node; an archive node (the reference devnet's alice) also serves the ten archive_v1_* methods, 120 in total. On top of the standard Substrate surface, the runtime declares one custom runtime API, ScalarCommonsApi, reachable through state_call.

Verified against scalar-commons spec 321, metadata v16.

Endpoints and transports ​

TransportURLUse for
WebSocketws://127.0.0.1:9944Anything with subscriptions, and all @polkadot/api work
HTTPhttp://127.0.0.1:9944One-shot curl queries, health checks, scripts

Subscriptions (*_subscribe*) require WebSocket. @polkadot/api needs it for its own runtime-upgrade subscription, so prefer ws:// for anything programmatic.

The reference devnet endpoint — alice, port 9944 — runs with --rpc-methods safe, set explicitly in deploy/systemd/scalar-alice.service. That withholds the unsafe method set — author_insertKey, author_rotateKeys, author_rotateKeysWithOwner, author_hasKey, author_hasSessionKeys, babe_epochAuthorship, system_addReservedPeer and friends — which answer with -32601 RPC call is unsafe to be called externally.

A node you start yourself does not behave that way. The flag defaults to auto, and auto serves the full method set, unsafe methods included, whenever RPC is listening on loopback; it falls back to the safe subset only when the listener is not local. So the keystore-mutating surface is exposed by default on any ordinary 127.0.0.1 node — no --rpc-methods unsafe opt-in required, and not binding --rpc-external does not withhold it. Anything that can open a loopback connection to the RPC port — another user on the box, a container sharing the network namespace, the far end of somebody's SSH tunnel — can call author_insertKey.

To get the devnet's posture on your own node, pass --rpc-methods safe yourself. See running a validator, which does, and which covers the one case where you need the unsafe set back.

A node's RPC port is not a public API

safe still exposes chain state and transaction submission. Put it behind a reverse proxy with TLS and rate limiting, or reach it through an SSH tunnel, rather than binding 0.0.0.0.

ScalarCommonsApi ​

An agent deciding whether to contract with a counterparty needs that counterparty's economic standing without replaying chain state itself. That data is spread across five pallets and two accumulator formulas, so the runtime assembles it in one call — which is also how off-chain agents and the indexer end up sharing one authoritative view instead of each deriving their own.

The API is read-only by construction: nothing here mints, moves or reserves.

MethodSignature
ScalarCommonsApi_get_agent_infoget_agent_info(who: AccountId32) -> Option<AgentInfo>
ScalarCommonsApi_get_era_metricsget_era_metrics() -> EraSnapshot
ScalarCommonsApi_get_oracle_scoreget_oracle_score(who: AccountId32, capability: u32) -> u32
ScalarCommonsApi_get_pending_emissionsget_pending_emissions(who: AccountId32) -> u128

Not surfaced as named JSON-RPC methods

These four are implemented in the runtime and exported by the WASM blob, but the node does not register JSON-RPC wrappers for them — that was explicitly out of scope when the RPC module was written. They are reachable via state_call (below), or as api.call.scalarCommonsApi.* in @polkadot/api, which does the encoding for you. Looking for scalar_getAgentInfo in rpc_methods will not find it, and that is expected.

Calling them from the SDK ​

@polkadot/api reads the runtime API declarations out of metadata v16, so the typed path needs no manual SCALE work:

ts
const info    = await api.call.scalarCommonsApi.getAgentInfo(address);
const metrics = await api.call.scalarCommonsApi.getEraMetrics();
const score   = await api.call.scalarCommonsApi.getOracleScore(address, capability);
const pending = await api.call.scalarCommonsApi.getPendingEmissions(address);

console.log(info.isSome ? info.unwrap().toJSON() : 'not an agent');

Calling them over raw JSON-RPC ​

state_call takes the runtime API method name and hex-encoded SCALE arguments. For a no-argument method the payload is "0x":

bash
curl -sH 'Content-Type: application/json' \
     -d '{"jsonrpc":"2.0","id":1,"method":"state_call",
          "params":["ScalarCommonsApi_get_era_metrics","0x"]}' \
     http://127.0.0.1:9944
json
{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000190000\
00dc05000088130000e803000000000000"}

The result is a SCALE-encoded EraSnapshot — 64 bytes, fixed-width, no length prefix because every field is fixed-size:

OffsetBytesFieldTypeDecoded above
04era_numberu320
44active_agentsu320
84ring_countu320
1216total_weightu1280
2816last_era_emissionu1280
444completion_fee_bpsu3225
484alphau321500
524betau325000
564floor_bpsu321000
604active_orchestratorsu320

All integers are little-endian. state_callAt takes a block hash as a third parameter for historical reads — which needs an archive node for anything beyond the pruning window.

Methods that take arguments need SCALE-encoded input, so AccountId32 is the 32-byte public key — not the SS58 string. Decode the address first (decodeAddress from @polkadot/util-crypto) and concatenate arguments in declaration order. This is the point at which using api.call.* stops being a convenience and starts being the sensible choice.

EraSnapshot fields ​

FieldTypeMeaning
era_numberu32Current era.
active_agentsu32Agents with any volume in the era preceding the last drain.
ring_countu32Ring-suspect agents at the last drain: active, established, ≤ 1 counterparty.
total_weightu128Sum of all agent weight snapshots — the denominator of your share.
last_era_emissionBalanceCMN emitted in the last settled era, in plancks.
completion_fee_bpsu32Live escrow completion fee.
alphau32Live governance-score weight in the emissions formula.
betau32Live work-score weight.
floor_bpsu32Live activity floor for qualifying agents.
active_orchestratorsu32Registered orchestrators.

total_weight is deliberately u128 and not Balance: it is a sum of scaled scores, not a token amount. Typing it as Balance would imply it is denominated in CMN. The last four fields are the auto-params values the chain has adjusted itself to — read them here rather than assuming genesis values still hold.

AgentInfo fields ​

get_agent_info returns None when the account holds no stake, meaning it is not a registered agent. An agent with stake but no metadata returns Some with empty uri and name — a different state, reported as such.

FieldTypeMeaning
stakeBalanceLocked agent stake. One factor of the weight, never the whole of it.
ranku32Ranked-collective rank 0–3. 0 also covers "not a member".
completionsu32Lifetime completed agreements. Drives rank promotion.
era_volumeBalanceEscrow volume this era. Reset on era drain.
unique_buyersu32Distinct counterparties this era — the anti-ring signal.
last_heartbeatBlockNumberBlock of the last heartbeat.
pending_emissionsBalanceClaimable right now. Advisory — see below.
heartbeat_multiplieru32Liveness multiplier, 10–100.
capabilitiesVec<u32>Declared capability IDs.
uriVec<u8>Off-chain endpoint. Empty when unset.
nameVec<u8>Display name. Empty when unset.
orchestratorOption<AccountId>Orchestrator this agent is a sub-agent of.

A high era_volume with unique_buyers ≤ 1 is the ring-farming shape, which is exactly why the weight formula multiplies volume by buyer diversity.

pending_emissions is advisory

It is reward already settled at earlier weights (emissions.agentPendingReward) plus accrual from the reward accumulator, at the instant of the call, clamped to remaining headroom under the supply cap. It is what you would receive claiming now — not a reservation. It moves with every era settlement.

The standard surface ​

Everything below is stock Substrate. Listed here because "which of these actually exist on this node" is otherwise a guess — mmr, beefy, statement, mixnet, sync_state, dev and state_trie_migration are all absent, either because the runtime has no corresponding pallet or because they were deliberately not wired.

Get the authoritative list from the node itself:

bash
curl -sH 'Content-Type: application/json' \
     -d '{"jsonrpc":"2.0","id":1,"method":"rpc_methods","params":[]}' \
     http://127.0.0.1:9944

Chain and system ​

MethodPurpose
system_healthPeer count, isSyncing. First call when debugging a node.
system_syncStatestartingBlock, currentBlock, highestBlock.
system_chain, system_name, system_versionChain and client identity.
system_propertiesss58Format, tokenDecimals, tokenSymbol.
system_chainTypeDevelopment / Local / Live.
system_peers, system_localPeerId, system_localListenAddressesNetworking state.
system_accountNextIndexNext nonce for an account, pending extrinsics included.
system_dryRun, system_dryRunAtApply an extrinsic without submitting it.
chain_getBlock, chain_getBlockHash, chain_getHeaderBlock retrieval.
chain_getFinalizedHeadLatest finalized hash. Not the same as chain_getHead.
chain_subscribeNewHeads, chain_subscribeFinalizedHeadsHead subscriptions.

system_accountNextIndex — exposed as account_nextIndex too — accounts for extrinsics still in the pool, which is what makes it the right source for a nonce when submitting several transactions back to back.

Best height is not finalized height

chain_getHead advances even when finality has stalled. A node that produces blocks without finalizing them looks healthy to a check that only reads best height. Watch chain_getFinalizedHead — that is what deploy/finality-check.sh samples.

State and storage ​

MethodPurpose
state_getMetadataRuntime metadata. The source of truth for every type shape.
state_getRuntimeVersionspecName, specVersion, transactionVersion, API hashes.
state_getStorage, state_getStorageAtRaw storage by key.
state_getKeys, state_getKeysPagedKey enumeration. Use the paged form.
state_queryStorageAtBatch several keys at one block.
state_call, state_callAtInvoke a runtime API — including ScalarCommonsApi.
state_subscribeStorageStorage change subscription.
state_getReadProofMerkle proof for a key set.
state_subscribeRuntimeVersionFires on runtime upgrade.

Never hand-write type shapes. Read state_getMetadata — or let @polkadot/api read it — because the runtime is the only authority on layout, and it changes with spec_version.

Author and transaction pool ​

MethodPurpose
author_submitExtrinsicFire-and-forget submission.
author_submitAndWatchExtrinsicSubmission with status updates. What the SDK uses.
author_pendingExtrinsicsCurrent pool contents.
author_rotateKeysGenerate session keys in the node's keystore. Runtimes before spec 313. Unsafe.
author_rotateKeysWithOwnerGenerate session keys plus the ownership proof session.setKeys checks from spec 313; takes the owner's 32-byte account id, returns { keys, proof }. Unsafe.
author_insertKeyInsert a key into the keystore. Unsafe.
author_hasKey, author_hasSessionKeysCheck what the keystore holds. Unsafe.
transaction_v1_broadcast, transactionWatch_v1_submitAndWatchNew JSON-RPC spec equivalents.

Consensus, payment, and the new JSON-RPC spec ​

MethodPurpose
babe_epochAuthorshipSlots this node is due to author. Unsafe.
grandpa_roundStateCurrent GRANDPA round and voter state.
grandpa_proveFinalityFinality proof for a block range.
grandpa_subscribeJustificationsJustification stream.
payment_queryInfoEstimated weight, class and partial fee for an encoded extrinsic.
payment_queryFeeDetailsInclusion-fee breakdown plus tip.
chainHead_v1_*, chainSpec_v1_*, archive_v1_*The new JSON-RPC spec surface, used by light clients.

The *_v1_* families are the modern spec and coexist with the legacy methods. Light-client work should target them; @polkadot/api still uses the legacy set.

Runtime APIs ​

Thirteen runtime APIs are declared in metadata. Twelve are standard; the thirteenth is this chain's own.

APIMethodsNotes
Core3version, execute_block, initialize_block
Metadata3Includes metadata_versions
BlockBuilder4
TaggedTransactionQueue1validate_transaction
OffchainWorkerApi1
SessionKeys2generate_session_keys, decode_session_keys
BabeApi6Includes equivocation report submission; the runtime processes reports from spec 316
GrandpaApi4Includes equivocation report submission; the runtime processes reports from spec 316
AuthorityDiscoveryApi1Current and next authority set
AccountNonceApi1account_nonce
TransactionPaymentApi4Fee estimation
GenesisBuilder3build_state, get_preset, preset_names
ScalarCommonsApi4Above

AuthorityDiscoveryApi returning the current and next authority set is what lets devnet validators find each other over the DHT with only one bootnode configured.

Pallet indices ​

Storage keys are derived from pallet names, but RuntimeCall and RuntimeEvent encoding uses the index. Anything decoding raw extrinsics or events needs these.

These indices are append-only. A renumbered or reused index silently changes what every encoded call means. A past bug in this repo had SafeMode, TxPause and Constitution duplicating 34–36 with Staking, Babe and Grandpa — which is why 38–41 are used and 34–36 are not what you would guess from reading declaration order.

PalletIndexRole
System0
Timestamp1
Balances4CMN balances
Historical6pallet_session::historical
AuthorityDiscovery8
Offences9
VoterList10pallet_bags_list
TransactionPayment13
Vesting14
Treasury15Completion fees, slash share
Sudo16Held by operator; removal scheduled for mainnet
Utility17
Multisig18
Scheduler19
Preimage20
Referenda21OpenGov
ConvictionVoting22OpenGov; the governance-score source
RankedCollective23Technical Council; the rank source
Identity25Required to register as an agent
Agents26Custom
Escrow27Custom
Oracle28Custom
Emissions29Custom
AutoParams30Custom
Orchestrator31Custom
NominationPools32
Whitelist33
Staking34
Babe35
Grandpa36
Session37
SafeMode38
TxPause39
Constitution40Custom — invariant monitoring
RankedPolls41pallet_referenda Instance2
Authorship99

pallet-constitution declares no extrinsics and no storage, so it appears in neither api.tx nor api.query. It is hooks and invariant checks only, which is why it still holds an index.

Reads by pallet ​

The custom pallets' storage, as api.query.<pallet>.<item>. Every entry here is also readable over state_getStorage with the hashed key.

PalletStorage
agentsagentStake, counterForAgentStake, stakeRegisteredAt, unstakeAt, completedAgreements, activeEscrowCount, eraEscrowVolume, eraUniqueBuyers, eraSeenBuyerSlots, eraGovParticipation, eraGovVotedPolls, eraRingSnapshot, eraActiveSnapshot, eraNumber, registrationsThisBlock, lastHeartbeat, pendingSlashAppeals, votingDelegations, agentMetadata, agentCapabilities
escrowagreements, nextSeq, disputeToAgreement, activeAgreementCount
oracleoracleRequests, counterForOracleRequests, oracleResponses, oracleResults, oracleAccuracy, oracleScore, eraFinalisedQuestions, eraTotalQuestions, capabilityQuestionCount
emissionsaccRewardPerStake, agentRewardDebt, agentWeightSnapshot, agentPendingReward, lastEraEmission, emissionOverrides, lastSettledEra, eraStartBlock
autoParamscompletionFeeBps, alpha, beta, floorBps, minScoreEligibleResponses, and a *Bounds entry per parameter
orchestratororchestratorRegistration, subAgentLinks, subAgentToOrchestrator, pendingLinkProposals, pendingProposalCount, eraOrchestratorVolume, orchestratorRewardDebt, orchestratorWeightSnapshot, orchestratorPendingReward, orchestratorGlobalAcc

Era-scoped maps (eraEscrowVolume, eraUniqueBuyers, eraGovParticipation, …) are drained at settlement. Reading them after an era boundary gives you the new era's partial totals, not the era that just paid out. For the settled figures read emissions.agentWeightSnapshot and emissions.lastEraEmission, or ScalarCommonsApi_get_era_metrics.

For the write side — every extrinsic of every custom pallet, with its constraints — see SDK usage.

Events and errors ​

Events are the indexer's input, and there are 25 REST endpoints built on them. Decode them from metadata rather than hardcoding variant indices: adding an event in the middle of a pallet's enum shifts every later index.

The events most worth subscribing to:

EventWhy
emissions.EraSettledEra number, total emission, total weight. The economic heartbeat.
emissions.RewardClaimedAn agent was paid.
emissions.CapReachedA claim was truncated by the supply cap.
escrow.AgreementCreatedCarries the seq you need for every later call on the agreement.
escrow.DeliveryConfirmedFunds moved; volume credited.
escrow.DisputeOpenedCarries the oracle request_id the dispute escalated to.
agents.EraMapsClearedEra boundary — era-scoped storage has just been drained.
autoParams.ParamAutoAdjustedA parameter moved, with old value, new value and reason.
constitution.SupplyCapApproachingIssuance entered the warning buffer.

Dispatch errors decode to pallet.Error with the doc comment attached, which is what the SDK surfaces. Most are deterministic: escrow.SelfDeal, emissions.EraNotDue and agents.StakeTooLow will fail identically on every retry, so treat them as answers rather than transient faults. See retries and failure.

Documented values verified against Scalar Commons Development (spec 321).