Skip to content

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.

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.

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.

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.

necter_dev.py
import base64, json, os
import requests
from eth_account import Account
from eth_account.messages import encode_defunct
RPC = "https://testnet-rpc.necter.network"
acct = Account.from_key(os.environ["NECTER_DEV_KEY"]) # a testnet-only wallet
ADDRESS = 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"]
Terminal window
export NECTER_DEV_KEY=0x… # never commit it; never paste it into a browser page
python -c 'import necter_dev as d; print(d.login()[:12], "…")'
from necter_dev import api, login
tok = 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).

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 manager

Use 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.