Skip to content
Solvers

Medusa API

Medusa is the service between solvers and the hub’s intent book. It streams open intents, accepts signed intents and solutions, and submits them to the hub in sequence.

One JSON-RPC 2.0 endpoint serves both plain HTTP POST and WebSocket. Use WebSocket for the subscription; any method can be called over either. The server pings WebSocket clients every 30 seconds. Khalani gives you the endpoint when it authorizes your solver address.

Send parameters as a positional array. Request types are camelCase. Notifications wrap their contents in snake_case keys (intents_solved, intent_id), but the intents inside them are camelCase, and the getHistory and getV2OrderByIntentId results are snake_case. Send uint256 values as decimal or 0x hex strings, since a JSON number above 2^64−1 is rejected; Medusa returns them as 0x hex. Enums are names, never numbers (see Enums). srcIdx, outIdx and inIdx are plain JSON numbers.

Method Params Returns
subscribeIntents SignedAddSolver A subscription: OpenIntents first, then updates (below)
unsubscribeIntents subscription id true
proposeSolution SignedSolution, SignedIntent[] (just-in-time intents), complementaryWithdrawal Hub transaction hash, as soon as Medusa accepts the solution

The address recovered from the credential or the solution signature must be an authorized solver. Pass false as complementaryWithdrawal. With true, after the solve Medusa withdraws each receipt that fills an Exact + AnySingle intent to its author’s origin spoke.

proposeSolution returns once Medusa has accepted the solution, and the hub transaction lands shortly after. A worked request, using the test vectors, with signatures and the remainder child shortened:

{
"jsonrpc": "2.0",
"id": 1,
"method": "proposeSolution",
"params": [
{
"solution": {
"intentIds": [
"0xc797464c9a24cc82b8cc0e4d466a58caa3502513ebd7814f34e9305b7112f037",
"0x8e346366bbb72f460cd1c1de1690beefd13ce5a983dbec1372d78a1349386ecd"
],
"intentOutputs": [{ "author": "0xa83d18aa3f32270762147e0e0f2cc3396a8f157a", "nonce": "3", "srcAmount": "400000000", "…": "the rest of O" }],
"receiptOutputs": [
{ "mToken": "0xbfd80b874e2215f8d2be0644c25271307cc28076", "mTokenAmount": "600000000", "owner": "0x5b38da6a701c568545dcfcb03fcb875f56beddc4", "intentHash": "0x8e346366bbb72f460cd1c1de1690beefd13ce5a983dbec1372d78a1349386ecd" },
{ "mToken": "0x6c3d9a02487d3b2d398bee6cb7c64be0b38955cc", "mTokenAmount": "598800000", "owner": "0xa83d18aa3f32270762147e0e0f2cc3396a8f157a", "intentHash": "0xc797464c9a24cc82b8cc0e4d466a58caa3502513ebd7814f34e9305b7112f037" }
],
"spendGraph": [
{ "srcIdx": 0, "outputIdx": { "outType": "Receipt", "outIdx": 0 }, "qty": "600000000" },
{ "srcIdx": 0, "outputIdx": { "outType": "Intent", "outIdx": 0 }, "qty": "400000000" },
{ "srcIdx": 1, "outputIdx": { "outType": "Receipt", "outIdx": 1 }, "qty": "598800000" }
],
"fillGraph": [
{ "inIdx": 0, "outIdx": 1, "outType": "Receipt" },
{ "inIdx": 1, "outIdx": 0, "outType": "Receipt" }
]
},
"signature": "0x…"
},
[
{
"intent": {
"author": "0x5b38da6a701c568545dcfcb03fcb875f56beddc4",
"validBefore": "1792440000",
"validAfter": "0",
"nonce": "2",
"srcMToken": "0x6c3d9a02487d3b2d398bee6cb7c64be0b38955cc",
"srcAmount": "598800000",
"outcome": {
"mTokens": ["0xbfd80b874e2215f8d2be0644c25271307cc28076"],
"mAmounts": ["1002004008016032064"],
"outcomeAssetStructure": "AnySingle",
"fillStructure": "PercentageFilled"
}
},
"signature": "0x…"
}
],
false
]
}

subscribeIntents takes one signed credential and returns a numeric subscription id:

{ "jsonrpc": "2.0", "id": 1, "method": "subscribeIntents", "params": [{ "payload": { "address": "0x5b38da6a701c568545dcfcb03fcb875f56beddc4", "validUntil": "1792440000" }, "signature": "0x…" }] }

Signed by an intent’s author (no authorization needed)

Section titled “Signed by an intent’s author (no authorization needed)”
Method Params Returns
proposeIntent SignedIntent [txHash, intentId]
cancelIntent signed { nonce, intentId } Hub transaction hash. intentId may be the original intent: Medusa cancels its current open remainder.
cancelAndPublish signed { nonce, cancelIds }, SignedIntent[] (all one author) Hub transaction hash
withdrawMtokens signed { address, amount, mtoken, nonce } Hub transaction hash
withdrawMtokensWithPayload signed { address, amount, mtoken, nonce, dstAddrBytes } Hub transaction hash

withdrawMtokens sends tokens to address on the mToken’s origin EVM spoke. For a non-EVM origin, use withdrawMtokensWithPayload: address is the EVM signer whose hub balance is debited, while dstAddrBytes is the recipient on the origin chain. Both requests take an 18-decimal mToken amount.

Sign the payload under the withdrawal domain in Signing. The second method uses WithdrawWithPayload(address address,uint256 amount,address mtoken,uint256 nonce,bytes dstAddrBytes). Encode dstAddrBytes as a 0x-prefixed hex string of the chain-native recipient bytes:

  • Bitcoin: the UTF-8 bytes of the Bitcoin address string.
  • TRON: the Base58Check-decoded 21-byte payload, including its 0x41 network prefix and excluding the checksum. Use an activated recipient account.
Method Returns
getIntent, getIntentStatus An intent, or its state
getHistory An intent’s history
getSolution, getSolutionsForSolver Submitted solutions
getIntentIdsByAuthor, getActiveIntentsByAuthor, getLiquidityIntentsByAuthor Intents by author
resolveIntentRoot The root of an intent’s partial-fill chain
getV2OrderByIntentId The API order an intent belongs to
getMtokenBalanceByAuthor(address, mToken) That address’s free (unescrowed) balance of one mToken
getSyncedBlockHeight The hub block Medusa has synced to
getNonce(address) A random uint256, not checked for use; your own random value is equivalent

Each notification arrives in a JSON-RPC envelope, with its body in params.result:

{ "jsonrpc": "2.0", "method": "subscribeIntents", "params": { "subscription": 4513198475290631, "result": { "NewIntent": Intent } } }

The body is an object with one key:

{ "OpenIntents": [Intent, …] }
{ "NewIntent": Intent }
{ "SolutionAccepted": { "intents_solved": [id, …], "new_intents": [Intent, …], "solver": "0x…" } }
{ "StatusChanged": { "intent_id": "0x…", "state": "Solved" } }

OpenIntents is always the first message and replaces your local book. Intents carry neither their id nor their state, so compute each id yourself (see Hashes). StatusChanged reports Solved, Cancelled or Expired. Expired follows Medusa’s own clock, so it can arrive a few seconds before the hub stops accepting the intent; a Cancelled for the same id follows once Medusa cancels it on the hub. In SolutionAccepted, intents_solved includes the solution’s just-in-time intents and new_intents holds its remainder children.

If your connection falls behind, the subscription ends with a final frame whose params.error is a plain string, and the socket stays open. Resubscribe with a new credential for a fresh snapshot:

{ "jsonrpc": "2.0", "method": "subscribeIntents", "params": { "subscription": 4513198475290631, "error": "subscription lagged behind the intent stream; resubscribe for a fresh snapshot" } }

Three schemes are in use. Hub constants: chain id 4278608, intent book 0x4411E3A41C12DC6cB205f7128879283081B99576, mToken manager 0x6d5Aa1e443cCA7eB43BA7D59Cc839f361fb87B08.

Intents are EIP-712 typed data:

const domain = { name: "KhalaniIntent", version: "1.0.0", chainId: 4278608, verifyingContract: INTENT_BOOK };
const types = {
Intent: [
{ name: "author", type: "address" },
{ name: "validBefore", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "srcMToken", type: "address" },
{ name: "srcAmount", type: "uint256" },
{ name: "outcome", type: "Outcome" },
],
Outcome: [
{ name: "mTokens", type: "address[]" },
{ name: "mAmounts", type: "uint256[]" },
{ name: "outcomeAssetStructure", type: "uint8" },
{ name: "fillStructure", type: "uint8" },
],
};

The intent id is the struct’s EIP-712 hashStruct, with no domain separator and no \x19\x01 prefix, so it is not the digest you sign. A contract author can sign an intent with ERC-1271.

Solutions are not EIP-712. Sign the raw hash keccak256(abi.encode(solution)), without the \x19 message prefix. Hashes gives the exact encoding, and the test vectors let you check it.

The subscription credential, cancels and withdrawals are EIP-712 under a second domain:

const domain = { name: "Arcadia", version: "1", chainId: 4278608, verifyingContract: INTENT_BOOK };
// AddSolver(address address,uint256 validUntil)
// CancelIntent(uint256 nonce,bytes32 intentId)
// CancelPublish(uint256 nonce,bytes32[] cancelIds)
// withdrawals use the same name and version with verifyingContract: MTOKEN_MANAGER
// Withdraw(address address,uint256 amount,address mtoken,uint256 nonce)
// WithdrawWithPayload(address address,uint256 amount,address mtoken,uint256 nonce,bytes dstAddrBytes)

Solutions and { payload, signature } credentials take only a 65-byte ECDSA signature, never ERC-1271. A signed payload is sent as { "payload": { … }, "signature": "0x…" }. Signed intents are { "intent": { … }, "signature": "0x…" } and signed solutions { "solution": { … }, "signature": "0x…" }.

Errors are standard JSON-RPC errors. Code -32009 (the message contains chain state unavailable) means Medusa could not accept a write while it resyncs with the hub, and any write method can return it. Nothing was executed or stored, so resend the same signed request unchanged after a short wait. Credential and parameter errors use -32602, and other failures -32603. Common messages:

Message Meaning
subscription credential expired validUntil is in the past.
validUntil more than 600s in the future Shorten the credential’s life.
subscription credential already used… Sign a new credential with a larger validUntil.
signer does not match address The credential was signed by a different key.
invalid signature: … The credential’s signature could not be recovered.
nonce already used A cancelIntent, cancelAndPublish, withdrawMtokens or withdrawMtokensWithPayload nonce was used before. These nonces are unique across all of Medusa, not per address, so use random 256-bit values.
solver authorization failed: … (subscribe), solver 0x… is not authorized (proposeSolution) The address is not an authorized solver. A solution signed over the wrong hash also shows up this way, because it recovers to another address.
invalid solution signature The signature is malformed.
proposeSolution failed: … The hub rejected the solution. The message continues transaction reverted on local executor: with the exit reason and the decoded custom error.