Sign in and enroll
The Hub has no passwords. You authenticate by signing a Sign-In with Ethereum (EIP-4361) message with your wallet and get a session token (a bearer token, valid 12 hours). For servers, create an API key.
In the store
Section titled “In the store”Connect your wallet at testnet.necter.network and sign the message. Then open Develop → Profile, fill in your developer type (individual or organization), display name and optional email, website and reason, accept the agreements and submit. On the testnet enrollment is verified automatically.
Through the API
Section titled “Through the API”POST /v1/auth/nonce {"address": "0x…lowercase", "domain": "testnet.necter.network"} → {"nonce", "issued_at", "expires_at", "message"} (single use, 300 s)POST /v1/auth/siwe {"message": "<the exact message>", "signature": "0x…"} → {"token", "address", "expires_at", "roles"}The message has the statement Sign in to Necter testnet., Chain ID: 11155111 and an allowed domain
(testnet.necter.network, 127.0.0.1:7878, localhost:7878 or necter-miner.app). Sign it as-is with
personal_sign.
A Python helper
Section titled “A Python helper”The deploy pages use this helper. It needs pip install requests eth-account and reads your developer key
from an environment variable. Use a dedicated testnet wallet.
import base64, json, osimport requestsfrom eth_account import Accountfrom eth_account.messages import encode_defunct
RPC = "https://testnet-rpc.necter.network"acct = Account.from_key(os.environ["NECTER_DEV_KEY"]) # a testnet-only walletADDRESS = acct.address.lower()
def api(method, path, token=None, **kw): headers = kw.pop("headers", {}) if token: headers["Authorization"] = f"Bearer {token}" r = requests.request(method, RPC + path, headers=headers, timeout=120, **kw) body = r.json() if r.content else None if r.status_code >= 400: raise RuntimeError(f"{r.status_code} {body}") return body
def personal_sign(text: str) -> str: sig = acct.sign_message(encode_defunct(text=text)).signature.hex() return sig if sig.startswith("0x") else "0x" + sig
def sign_typed(typed_data: dict) -> str: sig = Account.sign_typed_data(acct.key, full_message=typed_data).signature.hex() return sig if sig.startswith("0x") else "0x" + sig
def login() -> str: n = api("POST", "/v1/auth/nonce", json={"address": ADDRESS, "domain": "testnet.necter.network"}) return api("POST", "/v1/auth/siwe", json={"message": n["message"], "signature": personal_sign(n["message"])})["token"]export NECTER_DEV_KEY=0x… # never commit it; never paste it into a browser pagepython -c 'import necter_dev as d; print(d.login()[:12], "…")'Enroll
Section titled “Enroll”from necter_dev import api, logintok = login()dev = api("POST", "/v1/developers/me/enrollment", tok, json={ "developer_type": "individual", # or "organization" "display_name": "Ada's Lab", "agreements_accepted": True,})print(dev["enrollment"]["status"], dev["verification"]["status"]) # active verified"draft": true only saves the form. A second submission answers 409 invalid_state
(enrollment is already active). GET /v1/developers/me returns your record. Publishing a project requires
an active enrollment (403 not_enrolled otherwise) and a verified developer (403 not_verified).
API keys
Section titled “API keys”Keys are for backends: they never expire unless you set expires_at, and can be limited to scopes and
projects. The secret is shown once.
key = api("POST", "/v1/developers/api-keys", tok, json={ "name": "task-submitter", "scopes": ["tasks:submit", "analytics:read"], # tasks:submit, projects:read, projects:write, modules:write, analytics:read})print(key["secret"]) # nk_test_<8 chars>_<43 chars>; store it in your secret managerUse it as Authorization: Bearer nk_test_…. A key missing a scope answers 403 scope_missing. List keys
with GET /v1/developers/api-keys (secrets are never returned again) and revoke with
DELETE /v1/developers/api-keys/{key_id}. In the store: Develop → API keys.