Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/test-simplex-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,6 @@ jobs:
E2E_WORKDIR: ${{ runner.temp }}/simplex-e2e
E2E_BSC_TESTNET_RPC_URL: ${{ secrets.E2E_BSC_TESTNET_RPC_URL }}
E2E_POLYGON_AMOY_RPC_URL: ${{ secrets.E2E_POLYGON_AMOY_RPC_URL }}
# Hyperbridge's testnet bundlers. The RPC provider, Alchemy, does not serve
# EntryPoint v0.9 as a bundler.
E2E_BSC_TESTNET_BUNDLER_URL: https://bundler.polytope.technology/bsc-chapel
E2E_POLYGON_AMOY_BUNDLER_URL: https://bundler.polytope.technology/polygon-amoy
E2E_ORDERBOOK_URL: ${{ secrets.E2E_ORDERBOOK_URL }}
E2E_HYPERBRIDGE_WS_URL: ${{ secrets.E2E_HYPERBRIDGE_WS_URL }}
E2E_SOLVER1_PRIVATE_KEY: ${{ secrets.E2E_SOLVER1_PRIVATE_KEY }}
Expand Down
62 changes: 62 additions & 0 deletions docs/ai/changelog/2026-10-10-hyperbridge-bundlers-by-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 2026-10-10 — SDK 3.0.1 and simplex 0.17.1: Hyperbridge's bundlers by default

Hyperbridge runs ERC-4337 bundlers on Ethereum, BSC, Polygon, Base and Arbitrum, and on BSC Chapel
and Polygon Amoy for testnet. They need no API key and serve EntryPoint v0.8 and v0.9. The SDK and
simplex now use them without being told to.

## SDK

`ChainConfigData.bundlerUrl` in `src/configs/chain.ts` holds each chain's Hyperbridge bundler.
`EvmChain.bundlerUrl` returns the bundler the chain was given, or else that one. It is undefined on
a chain Hyperbridge runs no bundler for.

`EvmChainParams.bundlerUrl` and the second argument to `EvmChain.create()` stay optional, and a URL
passed there still wins. `IntentGateway` reads its bundler from the destination chain, so an
`IntentGateway` on a supported chain now submits fills and estimates fill gas through Hyperbridge's
bundler with no configuration.

## UserOperation fees

Fees are priced for rundler, which the Hyperbridge bundlers run. Rundler accepts an op paying at
least half the base fee, but only bundles one whose max fee covers its bundle base fee: the pending
base fee raised by `BUNDLE_BASE_FEE_OVERHEAD_PERCENT` (27% by default), plus the priority fee it
requires. An op priced below that is accepted and then skipped in every bundle until it expires.

`rundlerUserOperationFees` in the SDK's `rundlerFees.ts` asks the bundler for
`rundler_getUserOperationGasPrice` and uses its `suggested` fees, each raised by its bump
(`maxFeePerGasBumpPercent`, `maxPriorityFeePerGasBumpPercent`). A bundler that does not serve that
method is asked for `rundler_maxPriorityFeePerGas`, and the chain's gas price is raised to it as
before. `GasEstimator.estimateFillOrder` and simplex's `UserOpSender` both price this way.

The Pimlico and Alchemy pricing paths are gone, along with `BundlerMethod.PIMLICO_GET_USER_OPERATION_GAS_PRICE`
and the `PimlicoGasPriceEstimate` type.

## Simplex

Simplex no longer takes a bundler from its config. It submits every fill through Hyperbridge's
bundler for the chain. `HYPERBRIDGE_BUNDLER_URLS` in `src/config/bundlers.ts` lists them, and
`resolveChainConfigs` sets each resolved chain's `bundlerUrl` from it. The list repeats the SDK's
because the dashboard bundles that module without the SDK. A test checks the two match.

A chain Hyperbridge runs no bundler for can only be watched. Unless it is watch-only, the bundler
preflight refuses it at boot, when it is added, and when watch-only is turned off for it.
`PUT /api/chains` rejects it the same way.

Older configs still load. A `bundlerUrl` in `[[chains]]` is ignored, and `emitFillerToml` no longer
writes one, so the next save drops it.

Removed:

- `ChainInput.bundlerUrl` and `Simplex.chains.setBundlerUrl`.
- `FillerConfigService.setBundlerUrl`.
- The `simplex init` bundler step and its Pimlico helpers.
- `POST /api/setup/validate-bundler`, and `bundlerUrl` from the `validate-alchemy-key` results.

The wizards and the Chains panel offer only chains with a Hyperbridge bundler (`chainsForNetwork`),
so the testnet catalog is BSC Chapel and Polygon Amoy. `GET /api/chains` and `chains.list()` still
report each chain's `bundlerUrl`, now Hyperbridge's, and empty where there is none.

The testnet swap E2E no longer takes `E2E_BSC_TESTNET_BUNDLER_URL` or `E2E_POLYGON_AMOY_BUNDLER_URL`.
Its SDK client uses the default bundlers.

Simplex-desktop moves to 0.17.1 with simplex.
12 changes: 6 additions & 6 deletions docs/content/developers/evm/intent-gateway/placing-orders.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Use the same flow for other supported token pairs. The only difference between t
You need:

- An RPC URL for the source chain, and one for the destination chain if this is cross-chain.
- An ERC-4337 bundler URL for the destination chain. This is required in both modes because solvers submit fills as UserOperations.
- A destination chain Hyperbridge runs an ERC-4337 bundler for: Ethereum, BSC, Polygon, Base or Arbitrum. Solvers submit fills as UserOperations, and the SDK sends them through that bundler unless you pass your own.
- A source-chain wallet with the USDC input amount and enough native token for transaction gas and the quoted solver fee.
- A Hyperbridge-supported source and destination chain. `EvmChain.create()` rejects an RPC for a chain without a known Hyperbridge deployment.

Expand Down Expand Up @@ -148,9 +148,9 @@ import { createWalletClient, http } from "viem"
import { privateKeyToAccount } from "viem/accounts"

const RPC_URL = "https://base-mainnet.g.alchemy.com/v2/YOUR_KEY"
const BUNDLER_URL = "https://base-mainnet.g.alchemy.com/v2/YOUR_KEY"

const chain = await EvmChain.create(RPC_URL, BUNDLER_URL)
// Fills go through Hyperbridge's bundler for Base. Pass a second argument to use your own.
const chain = await EvmChain.create(RPC_URL)
const sourceChain = chain
const destChain = chain

Expand All @@ -170,11 +170,11 @@ import { privateKeyToAccount } from "viem/accounts"

const SOURCE_RPC_URL = "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"
const DEST_RPC_URL = "https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY"
const DEST_BUNDLER_URL = "https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY"

// The user signs on the source chain. The solver fills on the destination chain.
// The user signs on the source chain. The solver fills on the destination chain,
// through Hyperbridge's bundler for Arbitrum.
const sourceChain = await EvmChain.create(SOURCE_RPC_URL)
const destChain = await EvmChain.create(DEST_RPC_URL, DEST_BUNDLER_URL)
const destChain = await EvmChain.create(DEST_RPC_URL)

const coprocessor = await IntentsCoprocessor.connect("wss://nexus.rpc.polytope.technology")
const queryClient = createQueryClient({ url: "https://nexus.indexer.polytope.technology" })
Expand Down
15 changes: 7 additions & 8 deletions docs/content/developers/evm/simplex/api/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ curl -s $SIMPLEX/api/config
| Field | Description |
|---|---|
| `configPath` | The config file changes are saved to. Absent when the filler was started from a config object. |
| `toml` | The running config as TOML. The signer's key and API credentials, the Substrate key, Binance credentials, and API keys inside RPC and bundler URLs are masked. |
| `toml` | The running config as TOML. The signer's key and API credentials, the Substrate key, Binance credentials, and API keys inside RPC URLs are masked. |
| `logLevel` | The current log level. |
| `vaultConfigured` | Whether the solver started with a vault treasury. |
| `allowlistUsers` | The addresses on the order allowlist. Empty means every address is accepted. |
Expand All @@ -68,7 +68,7 @@ curl -s $SIMPLEX/api/chains
"chains": [
{ "chainId": 8453, "stateMachineId": "EVM-8453", "label": "Base",
"rpcUrls": ["https://base-mainnet.g.alchemy.com/v2/…", "https://…"],
"bundlerUrl": "https://…", "watchOnly": false, "running": true }
"bundlerUrl": "https://bundler.polytope.technology/base", "watchOnly": false, "running": true }
],
"catalog": [
{ "chainId": 1, "stateMachineId": "EVM-1", "label": "Ethereum", "network": "mainnet", "…": "…" }
Expand All @@ -79,20 +79,20 @@ curl -s $SIMPLEX/api/chains
```

<Callout type="warn">
Unlike `/api/config`, this route returns RPC and bundler URLs in full, with any API keys in them,
Unlike `/api/config`, this route returns RPC URLs in full, with any API keys in them,
so that the chain editor can send them back unchanged.
</Callout>

| Field | Description |
|---|---|
| `chains` | One entry per `[[chains]]` block in the config. `running` is `false` for a chain added since the solver started. It starts filling after a restart. |
| `catalog` | Every chain the setup wizard offers on this network, with its label, explorer and default RPC endpoints. |
| `chains` | One entry per `[[chains]]` block in the config. `bundlerUrl` is the [Hyperbridge bundler](/developers/evm/simplex/configuration#bundler) the chain fills through, empty where there is none. `running` is `false` for a chain added since the solver started. It starts filling after a restart. |
| `catalog` | Every chain the setup wizard offers on this network, which is every chain with a Hyperbridge bundler, with its label, explorer and default RPC endpoints. |
| `network` | `mainnet` or `testnet`. A solver runs on one network. |
| `globalWatchOnly` | `true` when `watchOnly` in `[simplex]` is a single on/off value for every chain. Per-chain `watchOnly` changes are then ignored. |

## PUT /api/chains

Replaces the chain set: which chains the solver runs on, their RPC endpoints, their bundlers, and which are watch-only. The body lists every chain you want to keep.
Replaces the chain set: which chains the solver runs on, their RPC endpoints, and which are watch-only. The body lists every chain you want to keep.

```bash lineNumbers
curl -s -X PUT $SIMPLEX/api/chains \
Expand All @@ -101,7 +101,6 @@ curl -s -X PUT $SIMPLEX/api/chains \
"chains": [
{ "chainId": 8453,
"rpcUrls": ["https://base-mainnet.g.alchemy.com/v2/KEY", "https://base-rpc.publicnode.com"],
"bundlerUrl": "https://bundler.polytope.technology/base",
"watchOnly": false }
]
}'
Expand All @@ -112,14 +111,14 @@ curl -s -X PUT $SIMPLEX/api/chains \
|---|---|
| `chainId` | The chain's numeric ID. |
| `rpcUrls` | At least one RPC URL, each on a different domain, as the [RPC quorum](/developers/evm/simplex/confirmations#rpc-quorum) needs. |
| `bundlerUrl` | Required. The bundler that submits fill UserOperations. The dashboard always sends the chain's [Hyperbridge bundler](/developers/evm/simplex/configuration#bundler). |
| `watchOnly` | Optional. Watch this chain without filling on it. |

Chains are wired up when the solver starts, so the new set is saved and takes effect on the next restart. The response always has `applied: false` and `restartNeeded: true`. `removed` lists the chains that were dropped.

Every chain is checked the way a restart would check it, and a failure answers `400` with nothing saved:

- Each RPC URL that is new for its chain must answer, and must report that chain's ID.
- A chain Hyperbridge runs no [bundler](/developers/evm/simplex/configuration#bundler) for must be watch-only.
- Each chain needs a confirmation policy. Mainnet chains have built-in ones, and testnet chains get a default.
- Every market's tokens must still resolve on the new chain set.
- A chain that still has a vault in the treasury cannot be dropped. Remove the vault first.
Expand Down
1 change: 0 additions & 1 deletion docs/content/developers/evm/simplex/api/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ curl -sN $SIMPLEX/api/events
| `GET` | [`/api/setup/orderbook`](/developers/evm/simplex/api/setup#get-apisetuporderbook) | The orderbook a new config posts to, and its books |
| `POST` | [`/api/setup/validate-alchemy-key`](/developers/evm/simplex/api/setup#post-apisetupvalidate-alchemy-key) | Check an Alchemy key and derive RPC URLs from it |
| `POST` | [`/api/setup/validate-rpc`](/developers/evm/simplex/api/setup#post-apisetupvalidate-rpc) | Check RPC endpoints |
| `POST` | [`/api/setup/validate-bundler`](/developers/evm/simplex/api/setup#post-apisetupvalidate-bundler) | Check a bundler endpoint |
| `POST` | [`/api/setup/validate-token`](/developers/evm/simplex/api/setup#post-apisetupvalidate-token) | Read a token's symbol and decimals |
| `POST` | [`/api/setup/derive-evm-address`](/developers/evm/simplex/api/setup#post-apisetupderive-evm-address) | The address of an EVM private key |
| `POST` | [`/api/setup/generate-substrate-key`](/developers/evm/simplex/api/setup#post-apisetupgenerate-substrate-key) | Generate a Hyperbridge account, or derive one's address |
Expand Down
21 changes: 2 additions & 19 deletions docs/content/developers/evm/simplex/api/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,25 +66,9 @@ curl -s -X POST $SIMPLEX/api/setup/validate-rpc \

Send `urls`, or a single `url`. `expectedChainId` is optional. The URLs must be valid and each on a different domain. When they are not, `results` is empty and `error` says why. Otherwise `ok` is `true` only when every URL answered for the expected chain, and each failing entry in `results` carries an `error`.

### POST /api/setup/validate-bundler

Checks that a bundler answers `eth_supportedEntryPoints` and, when `chainId` is sent, that it lists the EntryPoint the chain's solver account uses. Every finding is a warning, not a failure, so `ok` is `true` unless the URL is empty.

```bash lineNumbers
curl -s -X POST $SIMPLEX/api/setup/validate-bundler \
-H 'Content-Type: application/json' -H 'X-Simplex-UI: 1' \
-d '{ "url": "https://bundler.polytope.technology/base", "chainId": 8453 }'
# → { "ok": true, "entryPoints": ["0x…"] }
```

The response carries a `warning` when:

- the bundler does not answer, in which case there are no `entryPoints`, or its answer is not a list of addresses. Simplex runs with this bundler and logs a warning.
- it lists addresses without the chain's EntryPoint. Simplex refuses this bundler: the solver does not start, and adding the chain, setting this bundler URL or turning watch-only off for the chain fails.

### POST /api/setup/validate-alchemy-key

Checks an Alchemy API key, and derives an RPC URL for each mainnet chain Alchemy serves. Each chain also comes back with a `bundlerUrl`, which is the same Alchemy URL. The setup wizard and the dashboard no longer call this route: they use the [Hyperbridge bundler](/developers/evm/simplex/configuration#bundler) for every chain.
Checks an Alchemy API key, and derives an RPC URL for each mainnet chain Alchemy serves. The setup wizard and the dashboard no longer call this route.

```bash lineNumbers
curl -s -X POST $SIMPLEX/api/setup/validate-alchemy-key \
Expand All @@ -97,8 +81,7 @@ curl -s -X POST $SIMPLEX/api/setup/validate-alchemy-key \
"valid": true,
"chains": [
{ "chainId": 8453, "stateMachineId": "EVM-8453", "label": "Base",
"rpcUrl": "https://base-mainnet.g.alchemy.com/v2/KEY",
"bundlerUrl": "https://base-mainnet.g.alchemy.com/v2/KEY" }
"rpcUrl": "https://base-mainnet.g.alchemy.com/v2/KEY" }
]
}
```
Expand Down
18 changes: 7 additions & 11 deletions docs/content/developers/evm/simplex/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -139,31 +139,27 @@ Create an API keypair in the Turnkey Dashboard under user details. The <code>sig

## Bundler

Fills execute as ERC-4337 UserOperations, which a bundler submits on chain, so every `[[chains]]` entry needs a `bundlerUrl`. Hyperbridge runs a bundler for each of these chains. They need no API key and serve EntryPoint v0.8 and v0.9:
Fills execute as ERC-4337 UserOperations, which a bundler submits on chain. Simplex sends them through the bundler Hyperbridge runs for each chain, so there is nothing to configure. These bundlers need no API key and serve EntryPoint v0.8 and v0.9:

| Chain | `bundlerUrl` |
| Chain | Bundler |
|---|---|
| Ethereum | `https://bundler.polytope.technology/ethereum` |
| Base | `https://bundler.polytope.technology/base` |
| Arbitrum | `https://bundler.polytope.technology/arbitrum` |
| Polygon | `https://bundler.polytope.technology/polygon` |
| BNB Chain | `https://bundler.polytope.technology/bsc` |
| BSC Chapel (testnet) | `https://bundler.polytope.technology/bsc-chapel` |
| Polygon Amoy (testnet) | `https://bundler.polytope.technology/polygon-amoy` |

```toml lineNumbers
[[chains]] # Base
rpcUrls = ["https://..."]
bundlerUrl = "https://bundler.polytope.technology/base"
```

The setup wizards and the dashboard always use these bundlers and have no bundler field. `bundlerUrl` is edited only in `filler-config.toml`. A chain outside this list needs a bundler that serves the EntryPoint its solver account uses, set there; Simplex [refuses one that does not](/developers/evm/simplex/troubleshooting).
Simplex can only watch any other chain. It [refuses to fill there](/developers/evm/simplex/troubleshooting), so list such a chain under `watchOnly`. An older config's `bundlerUrl` is ignored.

## Delegation & Gas Payment

Simplex always uses the solver selection path: it submits a signed bid to Hyperbridge via `IntentsCoprocessor` rather than calling `fillOrder` directly. This requires `substratePrivateKey` and `hyperbridgeWsUrl` in `[simplex]`, plus a `bundlerUrl` on each `[[chains]]` entry.
Simplex always uses the solver selection path: it submits a signed bid to Hyperbridge via `IntentsCoprocessor` rather than calling `fillOrder` directly. This requires `substratePrivateKey` and `hyperbridgeWsUrl` in `[simplex]`.

Solver selection requires each solver's EOA to be delegated to the `SolverAccount` contract. Simplex performs this automatically at startup via EIP-7702:

- **Primary path** — builds a no-op UserOperation with an attached EIP-7702 authorization and submits it through the configured bundler. When a paymaster is deployed on the chain — Circle Paymaster (USDC) preferred, then the `SimplexPaymaster` (USDC or USDT), live on Ethereum, Arbitrum, Base, Polygon and BSC — see [Mainnet Contract Addresses](/developers/evm/contract-addresses/mainnet) — and the solver holds at least one whole token of a supported stablecoin, the paymaster pays gas in that stablecoin so the solver never needs native gas for delegation. Tokens with EIP-2612 are authorized by permit; tokens without it (such as BSC stables) need a one-time funded `approve(Permit2, max)` from the solver EOA, after which every operation carries a per-op Permit2 signature and no native gas is needed again.
- **Primary path** — builds a no-op UserOperation with an attached EIP-7702 authorization and submits it through Hyperbridge's bundler for the chain. When a paymaster is deployed on the chain — Circle Paymaster (USDC) preferred, then the `SimplexPaymaster` (USDC or USDT), live on Ethereum, Arbitrum, Base, Polygon and BSC — see [Mainnet Contract Addresses](/developers/evm/contract-addresses/mainnet) — and the solver holds at least one whole token of a supported stablecoin, the paymaster pays gas in that stablecoin so the solver never needs native gas for delegation. Tokens with EIP-2612 are authorized by permit; tokens without it (such as BSC stables) need a one-time funded `approve(Permit2, max)` from the solver EOA, after which every operation carries a per-op Permit2 signature and no native gas is needed again.
- **Fallback** — if the bundler path fails or the chain has no paymaster, Simplex sends a direct type-0x04 delegation tx using the solver's native balance. On paymaster-less chains it also keeps the ERC-4337 EntryPoint deposit topped up to cover `targetGasUnits` (default 3,000,000) at the current gas price.

No manual steps are required — delegation is idempotent and skipped if the EOA is already delegated to the correct contract.
Expand Down
1 change: 0 additions & 1 deletion docs/content/developers/evm/simplex/confirmations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ rpcUrls = [
"https://mainnet.infura.io/v3/YOUR_KEY",
"https://rpc.ankr.com/eth/YOUR_KEY",
]
bundlerUrl = "https://bundler.polytope.technology/ethereum"
```

When you configure more than one URL, every quorum-checked read (`eth_getLogs`, `eth_blockNumber`, receipt during confirmation counting) is accepted only when a **BFT quorum** of your endpoints — `floor(2N/3) + 1` of them — agree on the result. If the quorum can't be formed — endpoints down, throttled, or disagreeing — the call fails loudly with a `QuorumError` and the scan cursor does not advance. There is no pausing or ejection state: each call independently queries every endpoint and resolves the moment the quorum is met, so a chronically slow or hung endpoint settles out of band and never stalls the confirmation hot path, let alone shrinks the quorum. A provider that *answers with divergent data* is never special-cased away; it simply fails to join the agreeing group, which is what keeps a lying or reorged endpoint detectable rather than authoritative.
Expand Down
Loading
Loading