# Medusa API

> The JSON-RPC interface solvers use to watch the book, publish intents and submit solutions.

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.

## Connecting

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](/solvers/intent-reference/#enums)). `srcIdx`, `outIdx` and `inIdx` are plain JSON numbers.

## Methods

### For authorized solvers

| 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](/solvers/intent-reference/#test-vectors), with signatures and the remainder child shortened:

```json
{
  "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:

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

### 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 |

### Withdrawals

`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](#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.

### Public reads

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

## Subscription notifications

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

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

The body is an object with one key:

```json
{ "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](/solvers/intent-reference/#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:

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

## Signing

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

**Intents** are EIP-712 typed data:

```ts
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](/solvers/intent-reference/#hashes) gives the exact encoding, and the [test vectors](/solvers/intent-reference/#test-vectors) let you check it.

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

```ts
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

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