> For the complete documentation index, see [llms.txt](https://docs.swapkit.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.swapkit.dev/spotlights/chain-specific-guides/hypercore-signing-and-broadcasting.md).

# HyperCore signing & broadcasting

SwapKit supports **HyperCore** (Hyperliquid's exchange chain, `HYPE`) as a swap source and destination. Selling *from* HyperCore is the one flow where your wallet signs **EIP-712 typed data** and submits it to Hyperliquid itself, instead of building and broadcasting an ordinary chain transaction. HyperCore has no plain transfer primitive — value moves by signing a Hyperliquid *action* — so a deposit address alone is not actionable.

Depositing *into* HyperCore is unaffected: `X → HYPE.USDC` is a normal transaction on the sell chain, with the HyperCore account as the destination. Only the sell direction produces typed data.

### Chain & asset identifiers

HyperCore is chain **`HYPE`**. HyperEVM is a **separate** chain (`HYPEREVM`) and is not interchangeable with it.

| Identifier                                             | Notes                                                                                                                |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `HYPE.USDC-0xb88339CB7199b77E23DB6E890353E22632Ba630f` | The tradable HyperCore asset. The address is the **HyperEVM ERC-20 twin**, used as the canonical routing identifier. |
| `USDC:0x6d1e7cde53ba9467b783cb7c530ce054`              | The **HIP-1 token id**, used inside the signed action's `token` field. Not an EVM contract address.                  |

{% hint style="warning" %}
`HYPE` identifiers are **case-sensitive**. `Chain.Hype` is not an EVM chain, so no checksum normalization is applied — a lowercased contract address yields a misleading `noRoutesFound`.
{% endhint %}

Amounts are human-readable decimal strings everywhere, including inside the signed action (`"8"`, not base units).

### What /v3/swap returns

Discriminate on `meta.txType === "EIP_712_HYPE_SEND_ASSET"`. The `tx` object carries everything needed: the typed data to sign, the action to submit, and where to submit it.

```json
{
  "txHint": "simpleTransfer",
  "targetAddress": "0x4d434471b3337062c6cb74eaf8d5595dbbba6839",
  "swapId": "496b0ea3-4ff4-45ae-a222-75a0b4a3d2f7",
  "meta": { "txType": "EIP_712_HYPE_SEND_ASSET" },
  "fees": [
    {
      "type": "inbound",              // Hyperliquid account-activation fee — see below
      "amount": "1",
      "asset": "HYPE.USDC-0xb88339CB7199b77E23DB6E890353E22632Ba630f",
      "chain": "HYPE",
      "protocol": "FLASHNET"
    }
  ],
  "tx": {
    "typedData": {
      "domain": {
        "name": "HyperliquidSignTransaction",
        "version": "1",
        "chainId": 42161,
        "verifyingContract": "0x0000000000000000000000000000000000000000"
      },
      "primaryType": "HyperliquidTransaction:SendAsset",
      "types": {
        "HyperliquidTransaction:SendAsset": [
          { "name": "hyperliquidChain", "type": "string" },
          { "name": "destination",      "type": "string" },
          { "name": "sourceDex",        "type": "string" },
          { "name": "destinationDex",   "type": "string" },
          { "name": "token",            "type": "string" },
          { "name": "amount",           "type": "string" },
          { "name": "fromSubAccount",   "type": "string" },
          { "name": "nonce",            "type": "uint64" }
        ]
      },
      "message": {
        "hyperliquidChain": "Mainnet",
        "destination": "0x4d434471b3337062c6cb74eaf8d5595dbbba6839",
        "sourceDex": "spot",
        "destinationDex": "",
        "token": "USDC:0x6d1e7cde53ba9467b783cb7c530ce054",
        "amount": "8",
        "fromSubAccount": "",
        "nonce": 1786652874878
      }
    },
    "action": {
      "type": "sendAsset",
      "hyperliquidChain": "Mainnet",
      "signatureChainId": "0xa4b1",
      "destination": "0x4d434471b3337062c6cb74eaf8d5595dbbba6839",
      "sourceDex": "spot",
      "destinationDex": "",
      "token": "USDC:0x6d1e7cde53ba9467b783cb7c530ce054",
      "amount": "8",
      "fromSubAccount": "",
      "nonce": 1786652874878
    },
    "submitTo": "https://api.hyperliquid.xyz/exchange"
  }
}
```

`action` and `typedData.message` describe the same transfer. `signatureChainId` and `type` belong to the action envelope only — they are **not** part of the signed struct, which is exactly the eight fields listed in `types`.

### Signing

Sign `tx.typedData` with standard **`eth_signTypedData_v4`**. `primaryType` is given explicitly and `types` contains exactly that one struct — no `EIP712Domain` entry, so add one yourself if your signer requires it.

```typescript
const signature = await wallet.signTypedData(
  tx.typedData.domain,
  tx.typedData.types,
  tx.typedData.message,
);

const { r, s, v } = Signature.from(signature); // split the 65-byte signature
```

{% hint style="info" %}
The domain `chainId` is **42161 (Arbitrum)** and `verifyingContract` is the zero address. That is Hyperliquid's action-signing domain, not a contract call — nothing here touches Arbitrum, and everything stays on HyperCore. Don't "correct" it to 999 or 1337. `action.signatureChainId: "0xa4b1"` is the same 42161, hex-encoded, which is what Hyperliquid expects in the envelope.
{% endhint %}

### Broadcasting

POST to `tx.submitTo`, passing `action` through unchanged and reusing its `nonce`:

```json
{
  "action": { /* tx.action, byte-for-byte */ },
  "nonce": 1786652874878,
  "signature": {
    "r": "0x706b46cb902e5540d650b58955577d031341aca5d82be92db93c53a56d4681a5",
    "s": "0x512a9c2585bd9ffb776139dface8ee4776bc623c7c3e700c4d719e95df9cb89c",
    "v": 27
  },
  "vaultAddress": null
}
```

A success looks like `{"status":"ok","response":{"type":"default"}}`.

{% hint style="danger" %}
**Rejections also return HTTP 200.** A failed action comes back as `{"status":"err","response":"<reason>"}`. Checking only the HTTP status will report failures as successful swaps.
{% endhint %}

{% hint style="warning" %}
**`action.nonce` IS the submission nonce.** Signing one value and submitting a different one authenticates a different struct, and the transfer is rejected. Copy `action` through unchanged — do not re-order, re-derive, or re-normalize it after signing.

`nonce` is stamped server-side at `/v3/swap`, and Hyperliquid rejects nonces that drift far from its clock. Sign and submit promptly; if a payload has been sitting around, re-run `/v3/quote` → `/v3/swap` rather than reusing it.
{% endhint %}

### Why sendAsset, and what the dex fields mean

`sourceDex` and `destinationDex` name the HyperCore sub-account each side of the transfer touches: `""` is the default **perps** account, `"spot"` is the **spot** account.

* **`destinationDex` is always `""`.** Flashnet's `hypercore:USDC` route quotes default-perps USDC and cannot credit a spot deposit — a deposit sent with `"spot"` arrives but is not claimable by the route.
* **`sourceDex` is resolved for you.** SwapKit reads the sender's live perps and spot balances and picks whichever can fund the transfer, preferring perps.

{% hint style="warning" %}
Do not substitute Hyperliquid's `usdSend` or `spotSend` actions. Both are rejected outright on a **unified account** with `{"status":"err","response":"Action disabled when unified account is active"}`, and unified is Hyperliquid's recommended mode. `sendAsset` is the only transfer both account types can execute.

A unified account also reports every balance under `spotClearinghouseState`, with `clearinghouseState.withdrawable` reading `"0.0"` — so a balance check against the perps account alone will wrongly conclude the user has nothing.
{% endhint %}

### Account-activation fee

Hyperliquid charges the **sender** a one-time **1 USDC** fee when the destination account does not yet exist, **on top of** `amount`. Providers mint a fresh deposit address per swap, so in practice this applies to most HyperCore payins.

It is quoted as an `inbound` fee on the route (see `fees` above). The signed `amount` is unaffected — the user needs `amount + fee` available in the funding sub-account. When the balance falls short, the route carries an `insufficientBalance` warning and remains build-able, so present it before the user signs:

```json
{
  "code": "insufficientBalance",
  "display": "Insufficient balance",
  "tooltip": "The source account does not hold enough to cover this swap plus its network fees. The transaction can be built, but it will fail if submitted as-is."
}
```

### Verifying before you sign

Because the signature covers `typedData.message` while the funds move on `action`, a wallet should confirm the two agree before signing — every one of the eight signed fields, plus `action.destination` against the route's `targetAddress`. A disagreement means signing one transfer and submitting another.

### Tracking the payin

A `sendAsset` appears in Hyperliquid's ledger (`userNonFundingLedgerUpdates`) as `delta.type: "send"` — **not** `spotTransfer` or `internalTransfer`. Filtering for the older types will never match, and the transfer will look like it never happened:

```json
{
  "time": 1786652875123,
  "hash": "0x…",
  "delta": {
    "type": "send",
    "user": "0x05bf…",
    "destination": "0x4d43…",
    "sourceDex": "spot",
    "destinationDex": "",
    "token": "USDC",
    "amount": "8.0",
    "fee": "1.0",
    "feeToken": "USDC"
  }
}
```

Swap progress is tracked normally through [/track](/swapkit-api/track-request-the-status-of-a-swap.md), keyed on the deposit address.

### Don't confuse it with the Mayan HyperCore withdraw

Mayan's HyperCore route signs a different action (`HyperliquidTransaction:SendToEvmWithData`) under `meta.txType = EIP_712_HYPE_WITHDRAW`, in a `{domain, types, value}` shape — note `value`, not `message`, and no `primaryType`. That one **bridges off** HyperCore and is relayed by Mayan; this one stays on HyperCore and you submit it. The shapes are deliberately different so the two cannot be crossed — always branch on `meta.txType`.
