hypnex-forge
SDK for Forge — the paid tool registry on Morpheus AI. Implements MRC 60 Phase 1 (paid listings) and Phase 3 (on-chain per-call settlement via pre-paid escrow).
pip install hypnex-forge
# or
npm install hypnex-forgeStatus
| Component | Status | Where |
|---|---|---|
ForgeToolRegistry (Phase 1) | Live since 2026-05-09 | Arb + Base mainnet |
forge.hypnex.xyz UI (Phase 2) | Live | forge.hypnex.xyz ↗ |
ForgeEscrow (Phase 3) | Live since 2026-05-09 | Arb + Base mainnet (verified) |
relayer.hypnex.xyz (Phase 3) | Live | See apps/relayer/ |
Deployed addresses
| Contract | Arbitrum One | Base mainnet |
|---|---|---|
ForgeToolRegistry (Phase 1) | 0xa08ca3…013D2 | 0x0c617b…45aC |
ForgeEscrow (Phase 3) | 0x88E32F…89Da5 | 0xCC258a…454F |
Per-chain MOR token addresses are baked into MOR_ADDRESSES and used as the fee token by both contracts.
Phase 1 — list a tool
A one-time 1 MOR fee (admin-settable) routes directly to Hypnex Labs in the same register() tx. Per-call revenue stays with the tool owner; with Phase 3 on, settlement adds a small protocol fee.
from hypnex_forge import Forge
f = Forge(chain="arbitrum", private_key="0x...")
tx = f.register(
namespace="myorg",
name="websearch",
mcp_uri="https://mcp.example.com/v1",
categories=["search", "data"],
price_per_call_wei=10**15, # 0.001 MOR/call (end users pay you)
)import { Forge } from "hypnex-forge";
const f = Forge.withPrivateKey(process.env.PK as `0x${string}`, { chain: "arbitrum" });
await f.register({
namespace: "myorg",
name: "websearch",
mcpURI: "https://mcp.example.com/v1",
categories: ["search", "data"],
pricePerCallWei: 10n ** 15n,
});Phase 3 — pre-paid escrow + per-call settlement
Three actors, four phases:
agent ──deposit──▶ ForgeEscrow ◀──signature── tool owner
▲ ▲
│ │
relayer.hypnex.xyz ◀────POST /receipt─┘
│
└─── settle() every 4h ──▶ chain
│
├──▶ 90% to tool owner
└──▶ 10% to Hypnex treasuryThe protocol fee is 10% at launch, hard-capped at 20% in the contract bytecode — admin cannot rug. Each receipt is signed off-chain over:
keccak256(abi.encode(chainid, escrow, toolId, agent, callCount, periodId))wrapped with EIP-191. The contract recovers the signer using the same encoding and rejects mismatches.
Tool owner — register a signer
from hypnex_forge import Escrow, tool_id
esc = Escrow(chain="arbitrum", private_key=os.environ["TOOL_OWNER_PK"])
esc.set_signer(tool_id("myorg", "websearch"), "0xYourSessionKeyOrEoa")Agent — pre-fund per-tool escrow
Auto-approves MOR allowance on first call:
esc = Escrow(chain="arbitrum", private_key=os.environ["AGENT_PK"])
esc.deposit(tool_id("myorg", "websearch"), 10 * 10**18) # 10 MOR runwayTool server — sign + post a receipt
After every N calls, the tool's authorized signer key signs a receipt and posts it to the relayer:
esc.call_with_receipt(
tool_id=tool_id("myorg", "websearch"),
agent="0xAgentAddress",
call_count=100, # cumulative calls in this period
period_id=42, # strictly increasing per (toolId, agent)
)The Hypnex relayer batches receipts and submits settle() on a 4-hourly cron. Each settle() debits callCount * pricePerCallWei from the agent's escrow and routes 90 / 10 to the tool owner / treasury.
Writing your own (non-Hypnex) receipt client
The official SDKs handle this for you. If you POST to relayer.hypnex.xyz/receipt from your own client, you must send a User-Agent header — Cloudflare's default WAF rejects generic Python-urllib/X.Y and Node fetch UAs with HTTP 403 + error code: 1010 before the request ever reaches the Worker. Set anything reasonable, e.g. User-Agent: my-tool-server/1.0 (+https://example.com).
Receipt format (signed by the tool's setSigner key):
keccak256(abi.encode(chainid, escrow, toolId, agent, callCount, periodId))wrapped in the EIP-191 \x19Ethereum Signed Message:\n32 prefix. POST body schema:
{
"chain": "arbitrum" | "base",
"escrow": "0x...",
"toolId": "0x...",
"agent": "0x...",
"callCount": "100",
"periodId": "42",
"signature": "0x..."
}Tool owner — withdraw accrued revenue
print(esc.tool_revenue(tid) / 1e18, "MOR pending")
esc.withdraw_tool(tid, "0xColdWallet")Agent — 24h-delayed self-custody exit
If a tool owner misbehaves or you want to stop using a tool, request a delayed exit. The relayer has 24h to flush pending settlements before you can pull whatever's left:
esc.agent_request_withdraw(tid) # opens the 24h window
# ... 24h later ...
esc.agent_execute_withdraw(tid, "0xAgentAddress")Pure helpers (no RPC)
Compute IDs off-chain — they match the on-chain encoding 1:1:
from hypnex_forge import tool_id, category_tag, receipt_struct_hash
tool_id("hypnex", "websearch") # bytes32 keccak256("hypnex/websearch")
category_tag("search") # bytes32 keccak256("search")
# Phase 3 receipt hash (pre-EIP-191):
receipt_struct_hash(
chain_id=42161,
escrow_address="0x...",
tool_id=tool_id("myorg", "websearch"),
agent="0xAgent",
call_count=100,
period_id=42,
)import { toolId, categoryTag, receiptStructHash } from "hypnex-forge";
receiptStructHash({
chainId: 42161,
escrow: "0x...",
toolId: toolId("myorg", "websearch"),
agent: "0xAgent",
callCount: 100n,
periodId: 42n,
});The TypeScript and Python helpers produce byte-identical output to the Solidity keccak256(abi.encode(...)) recovery — verified by 29 Foundry tests + 7 anvil-based Python E2E tests + 26 worker tests.
Discovery
# All tools owned by a wallet
f.tools_of("0x...")
# All tools in a category
f.tools_in_category("search")
# Pagination over all tools (cheap)
f.list_tools(offset=0, limit=50)Source
- SDKs: python-forge/ · typescript-forge/
- Solidity: registry-contracts/contracts/ForgeToolRegistry.sol · ForgeEscrow.sol
- Relayer: apps/relayer/
- Deploy records: FORGE_DEPLOYMENT.md · FORGE_ESCROW_DEPLOYMENT.md
- UI: forge.hypnex.xyz ↗