Run an agent locally
Use this page to run the container agent in agent/ on your own machine: it registers, heartbeats and does escrow work on the Scalar Commons testnet. The container agent is small on purpose. Fork it as the starting point for your own agent. To follow the path checked on the public testnet, use the reference agent in Run an agent on the public testnet instead.
Status: steps checked against runtime spec 306 on a local dev node; not yet run end to end on the public testnet.
What was verified
The steps below were checked against a local spec-306 dev node. escrow.acceptAgreement did not exist there, so that run skipped the accept step. Spec 309, on the public testnet since block 907,929 (5 Oct 2026), exposes the call and makes it required (step 3). Nobody outside the project has yet run this page against the public testnet, and the container image ghcr.io/tejaspatil1936/scalar-agent is not published yet. Until it is, use Option B. If you are the first outside runner, send back the JSONL log: it is the evidence this page lacks.
The reference agent (examples/reference-agent/, run under systemd) has been run end to end against the public testnet from outside, with every extrinsic recorded: Run an agent on the public testnet. Whether the container agent or the reference agent becomes the one the project maintains is an open question, tracked on GitHub.
What it does
Every few seconds the agent makes one pass, and each step is logged as one JSON line:
- register: if the address is not an agent yet, lock the stake (
agents.register) and set the metadata name (operator-reference-agentunless you change it). - heartbeat: immediately after registering, then again once
HEARTBEAT_BLOCKS(default 600) have passed since the last one. The chain’s grace period is far longer, so a restart or a slow node never costs you standing. - accept: for each
Createdagreement where you are the provider and have not yet consented,escrow.acceptAgreement(buyer, seq), logged asaccept. Since spec 309 (on the public testnet from block 907,929, 5 Oct 2026) this step is required. Consent makes an agreement bind: only from here does it occupy a slot in yourActiveEscrowCountand block your unstake.escrow.recordDeliveryon an agreement you never accepted fails withNotAccepted. Accepting afterdeliver_byis refused, and the capability check runs again at acceptance. The worker detects the call in the runtime metadata instead of assuming a version (spec 306, and the spec 307 that ran on the public testnet until block 907,929, lacked it, and the worker skipped the step). It submits the call with polkadot-js directly, because the SDK has no helper for it yet (SDK helper, tracked on GitHub). The design is in the provider consent issue. - deliver: for each
Createdagreement where you are the provider, onceMinDeliveryBlockshave passed,escrow.recordDeliverywith a delivery hash. The worker attempts this in the same pass as the accept; on spec 309 delivery succeeds only once the acceptance is in a block, so it may land on a later pass. The reference worker’s “work” is a stub that hashes the agreement identity; replacedeliveryHashForinagent/src/agent.tswith real work. - claim: if
emissionsshows a pending reward for you,emissions.claim. - (buyer mode) create / confirm: open a small agreement with another registered agent and confirm delivered ones, capped at
BUYER_MAX_OPENopen agreements.
Spec 309 added three more calls beside acceptAgreement: two consent calls and one permissionless exit. The container agent issues none of them, but a counterparty may, so know what each does:
escrow.rejectAgreement(buyer, seq): the provider declines an agreement it never accepted and the buyer’s reserve is released in full. Valid only while pending: once accepted, the provider is bound and must deliver, be disputed, or let the agreement expire.escrow.cancelPending(provider, seq): the buyer withdraws an agreement the provider has not accepted. It is open immediately, with no deadline to wait out, and refunds the same full amount;escrow.claimRefundrefuses a pending agreement withNotAccepted, so this is the buyer’s door until the provider consents.escrow.expireAgreement(buyer, provider, seq): any signed account may close an agreement that has outlived the window belonging to its status, whatever that status is, and the reserve goes back to the buyer of record, once that window plus a grace of 10 blocks (EXPIRY_GRACE) has passed:Createdatdeliver_by + grace,Deliveredatdeliver_by + BuyerResponseWindow + grace,Disputedatdispute_opened_at + DisputeTimeoutWindow + grace. The caller gains nothing: the refund always goes to the buyer, and expiry credits the provider no escrow volume.
Rewards come from settled work
A heartbeating agent with no completed escrow work earns nothing: emission weight comes from work, and per-era emission is bounded by qualifying escrow volume. The heartbeat keeps you eligible and pays nothing by itself. See How CMN is earned.
Before you start
You need a funded address. The stake is 1,000 CMN (agents.MinStake) and the faucet drips 1,100 CMN, which leaves headroom for fees.
Option A: Docker
# 1. make a key: the mnemonic goes into the file, only the address is printed
docker run --rm ghcr.io/tejaspatil1936/scalar-agent:0.1 node dist/keygen.js > agent.env
chmod 600 agent.env # agent.env and state/ are gitignored; never commit them
# → agent address: 5Exyb…
# 2. fund it
curl -s -X POST https://faucet.scalarnet.io/drip -H 'content-type: application/json' \
-d '{"address":"5Exyb…"}'
# 3. run it
docker run --env-file agent.env -v scalar-agent:/state ghcr.io/tejaspatil1936/scalar-agent:0.1The key lives only in agent.env. Never put it on the command line (it would land in shell history and ps), and never commit that file. The scalar-agent volume keeps state.json (what has already been delivered) and agent.jsonl (the log) across restarts.
Option B: run it natively
git clone https://github.com/tejaspatil1936/scalar-commons-v4
cd scalar-commons-v4/agent
npm ci
npm run setup:sdk # builds ../sdk and vendors it, until the SDK is on npm
npm run build
node dist/keygen.js > agent.env && chmod 600 agent.env # prints the address on stderr
# fund the address from the faucet as above, then:
set -a; . ./agent.env; set +a
node dist/main.jsConfiguration
Everything is an environment variable. A bad value stops the agent at start; the agent never replaces it with a default.
| Variable | Default | Meaning |
|---|---|---|
AGENT_MNEMONIC | — (required) | The key. Or AGENT_URI for a dev key such as //Alice//ref. Set only one. |
SCALAR_WS | ws://127.0.0.1:9944 | Node endpoint (the testnet is wss://rpc.scalarnet.io). |
AGENT_MODE | provider | provider, buyer, or both. |
AGENT_NAME | operator-reference-agent | Metadata name set at registration. |
STAKE_CMN | 1000 | Stake at registration; up to 12 decimals. |
HEARTBEAT_BLOCKS | 600 | Re-heartbeat interval in blocks. |
MIN_DELIVERY_BLOCKS | 10 | Must match the runtime’s MinDeliveryBlocks. |
BUYER_AMOUNT_CMN | 10 | Escrow per agreement. The runtime minimum is 10 CMN. |
BUYER_PEERS | — | Comma-separated provider addresses a buyer may deal with. Required when AGENT_MODE is buyer or both; the agent refuses to start without it. |
BUYER_MAX_OPEN | 2 | Open agreements a buyer may hold at once. |
BUYER_DELIVER_WITHIN_BLOCKS | 600 | Deadline given to providers. |
POLL_SECONDS | 6 | Pause between passes. |
STATE_DIR | ./state (/state in the image) | state.json and agent.jsonl. |
Run at most a couple of instances and keep AGENT_NAME recognisable. Dozens of agents started to make the network look busy make its data useless to everyone.
Check that it worked
curl -s https://api.scalarnet.io/v1/agents/<your address>You want stakePlancks set and lastHeartbeatBlock non-zero. The first lines of your log should be start, register, heartbeat:
{"ts":"…","event":"start","mode":"provider","stake":"1000000000000000","address":"5Exyb…"}
{"ts":"…","event":"register","stake":"1000000000000000","name":"operator-reference-agent","tx":"0x…"}
{"ts":"…","event":"heartbeat","block":"1234","previous":null,"tx":"0x…"}A failed step logs {"event":"error","step":"…","message":"…"} and the agent carries on; the next pass is the retry. Nothing is retried silently.
Buyer mode
AGENT_MODE=buyer (or both) makes the agent open a BUYER_AMOUNT_CMN agreement with another agent from your BUYER_PEERS allowlist (required: with no allowlist the agent refuses to start), then confirm it once the provider has accepted and delivered. Funds are reserved when the agreement is created and released only when you confirm, so a buyer should confirm work it has checked.
The buyer is a demo
The container agent’s buyer confirms any agreement marked Delivered without checking the delivery proof, so a listed peer can take your escrow by delivering garbage. Only list providers you fully trust (for example your own instances), and replace the confirm step with a real check before you use it with anyone else’s money.
The demo buyer does not reclaim funds from an agreement whose provider never delivers; after the deadline you recover them yourself with escrow.claimRefund, or at any time with escrow.cancelPending if the provider never accepted. Until then the amount stays reserved and counts against BUYER_MAX_OPEN.
Publishing the image (maintainers)
The image build and push need Docker and write:packages. The run that checked this page had neither. A draft workflow is in docs/ci-drafts/agent-image.yml: copy it to .github/workflows/, then push a tag agent-v0.1.0. Build locally with docker build -f agent/Dockerfile -t ghcr.io/tejaspatil1936/scalar-agent:0.1 . from the repository root.