Operators

Reference Solver

Installation, configuration, pricing, wallets, persistence and operations for the supplied Anyflo Solver application.

Overview

The Reference Solver is a standalone, long-lived Bun process for liquidity providers. It connects to SolverHub, bids on supported Order objects, verifies source deposits when enabled, and signs destination-chain payouts. It stores orders, bids and payout state in local SQLite.

This manual describes the supplied application, not requirements imposed on every solver implementation. See the liquidity-provider overview for the business case, Integration for enrollment and a first test order, or SolverHub API to implement your own client. @anyflo/solver is an application, not an embeddable Solver SDK.

Installation

Install Bun 1.4.4 or newer. The application requires Bun and runtime dependencies; it is neither a native standalone binary nor a Node-only application.

Obtain registry access and an exact released version from Anyflo during onboarding. A source manifest version does not establish that a package has been published; anonymous public installation is not assumed. Set SOLVER_VERSION to the confirmed release before using the commands below.

Add the scoped registry to your user-level .npmrc, preserving existing settings and the default registry for third-party packages:

@anyflo:registry=https://gitlab.com/api/v4/projects/85458236/packages/npm/

Then you can install the package globally:

bun add --global "@anyflo/solver@$SOLVER_VERSION"
anyflo-solver --version
anyflo-solver --help

If the command is missing from PATH, run bun pm bin -g and add the reported directory. Do not use the installed package directory for operator configuration or state.

For a project-pinned deployment, use a new deployment project:

bun init -y
bun add --exact "@anyflo/solver@$SOLVER_VERSION"
bun run anyflo-solver --help

Keep its manifest and lockfile, deploy with bun install --frozen-lockfile, and substitute bun run anyflo-solver for anyflo-solver below. Integration covers creating the persistent operator directory and initial enrollment.

CLI

anyflo-solver
Start the daemon; equivalent to run.

anyflo-solver run [--verify-txs] [--run-approve-txs]
Start with optional enabling overrides. The flag-only form without run also works.

anyflo-solver init
Create missing solver.config.json and bidding-price.jsonc; retain existing paths unchanged. Templates require deployment-specific editing.

anyflo-solver gen-auth-key [--key-id <int>] [--solver-id <int>] [--keyrecord]
Generate an Ed25519 authentication key. Key ID defaults to 1; --keyrecord requires --solver-id and writes a public registration record.

anyflo-solver migrate
Apply local SQLite migrations without starting the daemon or loading private keys. Optional: normal startup already applies migrations.

anyflo-solver --help, -h, or <command> --help
Print help without loading operator configuration.

anyflo-solver --version
Print the installed version; use this option alone.

The run flags enable settings; omission preserves configured values. Unknown commands/flags, extra positional arguments and command failures exit nonzero.

Key-generator stdout contains the private key. Do not send it to shared logs or redirect it over an existing wallet .env. Securely retain the printed SOLVER_AUTH_PRIVATE_KEY assignment. Send only the public solver-registerkey-<solverId>-<kid>.json record to Anyflo for initial registration. Generating a key does not register it. Subsequent rotation follows SolverHub key rotation.

Configuration

The main file is solver.config.json in the working directory; SOLVER_CONFIG_PATH overrides its location. It is JSON, while the separate pricing file supports JSONC. The configuration properties below belong to the Reference Solver application, not the hosted SolverHub API’s Order object. Obtain the assigned solverId, solverHubWsUrl, supported routes, token identifiers and deployment addresses from Anyflo; do not infer production settings from development templates. See the minimal integration configuration rather than keeping unconfigured template chains.

Main configuration

JSON fieldType and requirementDefault / meaning
solverIdRequired positive numberAssigned solver identity; use the provisioned positive integer ID.
solverHubWsUrlRequired WebSocket URLAssigned full WSS URL including /connect; schema accepts ws:// or wss://.
verifyTxsRequired booleanEnables source-deposit verification. Keep enabled for independently verified payouts.
runApproveTxsBooleanfalse; per-order EVM approval transactions when enabled.
evmChainsArray of EVM configurations below[]; configure every relevant source and destination EVM chain.
suiOptional Sui configuration belowOmitted; only one Sui configuration.
dbPathOptional string"solver-state.db"; relative to the working directory, not the configuration file.
ordersTTLPositive number, milliseconds86400000 (24 hours); retention for un-won orders and bids.
myOrdersTTLPositive number, milliseconds1814400000 (21 days); retention for executed/archived orders won by this solver.
sweepIntervalMsPositive number, milliseconds600000 (10 minutes); database sweep interval.
retryPendingIntervalMsNonnegative number, millisecondsEffective default 30000; 0 disables startup and periodic pending-order retries.
finalityPoliciesOptional record of chain policiesDeposit-verification overrides described below.
alertObject with sinks array{"sinks":[]}; see Monitoring and alerts.

Retention settings govern eligible archived/terminal records, not a reason to discard unresolved payout state. Main configuration and secret changes require process restart; only the pricing file is hot-reloaded.

EVM chain configuration

Each entry in the Reference Solver’s evmChains configuration array has these fields (for example, evmChains[].solverAddress):

FieldType / meaning
chainRequired supported EVM chain ID from the list below.
rpcUrlRequired URL for your chain RPC.
depositAccountAddressRequired EVM address of the source deposit contract.
deliveryAccountAddressRequired EVM address of the destination delivery contract / token spender.
solverAddressRequired EVM address supplied as OrderAssignAck.payload.solverAddress and recorded as Order.solverAddress for source receipts; not the destination payout signer or delivery contract.
supportedTokensRequired array of EVM token addresses, not symbols.
directDepositBoolean, default false; direct transfer versus credited source receipt.

The transaction-signing private key comes from EVM_<numeric-chain-id>_PRIVATE_KEY, not a JSON field; its derived wallet funds and signs payouts on that chain. For local eip155:31337, loading also requires that derived address to match evmChains[].solverAddress.

Sui configuration

Field in Reference Solver configuration suiType / meaning
chainRequired supported Sui chain ID below.
rpcUrlRequired RPC URL.
depositPackageIdRequired valid Sui object ID for the deposit package.
deliveryPackageIdRequired valid Sui object ID for the delivery package.
registryIdRequired valid Sui registry object ID.
solverAddressRequired string supplied as OrderAssignAck.payload.solverAddress and recorded as Order.solverAddress for source receipts; separate from the payout signer derived from SUI_PRIVATE_KEY.
supportedTokensRequired array of coin-type strings, not EVM token addresses or symbols.
startCheckpointRequired string for the configured starting checkpoint.

Accepted chain IDs

Schema support is not a promise that a route, token or contract is provisioned in your environment.

EnvironmentAccepted IDs
MainnetsBase eip155:8453, Polygon eip155:137, Arbitrum eip155:42161, Sui sui:mainnet
TestnetsBase Sepolia eip155:84532, Polygon Amoy eip155:80002, Arbitrum Sepolia eip155:421614, Sui sui:testnet
Development / localeip155:31337, sui:devnet, sui:localnet

Deposit finality policies

finalityPolicies is a record keyed by chain ID. Each policy contains chain (a supported chain ID) and a nonempty tiers array. A tier has optional nonnegative numeric maxAmountUsd and required requiredFinality, exactly "latest", "safe" or "finalized". An omitted maximum covers amounts above preceding tiers. Set the record key and policy’s chain consistently.

This configuration fragment illustrates the schema, not a recommended risk policy:

{
  "finalityPolicies": {
    "eip155:84532": {
      "chain": "eip155:84532",
      "tiers": [
        { "maxAmountUsd": 100, "requiredFinality": "safe" },
        { "requiredFinality": "finalized" }
      ]
    }
  }
}

These are source-deposit verification policies, not SolverHub transport deadlines or a guarantee of immediate finality. Without overrides, the verifier uses the implementation’s chain-specific default policies. Assess RPC support and finality latency for every enabled route.

Secrets

Inject secrets through a protected environment or a restricted working-directory .env, which Bun loads. Never commit this file, private JWKs, wallet keys, registry tokens or captured JWTs. Supply wallet keys only for configured chains.

Environment variableFormat / purpose
SOLVER_AUTH_PRIVATE_KEYJSON object {"kid":<positive integer>,"jwk":<private Ed25519 JWK>}. Authentication/signing identity registered with Anyflo.
EVM_<numeric-chain-id>_PRIVATE_KEY0x-prefixed hex wallet private key; for example EVM_84532_PRIVATE_KEY.
SUI_PRIVATE_KEYSui Ed25519 secret in Bech32 (suiprivkey1...) or base64-encoded 32-byte seed form. Required only with sui.

Authentication keys and transaction wallet keys serve different purposes. Rotating an authentication key does not rotate a wallet or move inventory. Follow the add-new, switch, verify, revoke-old procedure; update the injected authentication secret and restart the process. Losing the last active authentication key requires Anyflo-assisted recovery.

Other environment settings are not secret-key JSON fields:

VariableMeaning
SOLVER_CONFIG_PATHOverride main configuration location.
BIDDING_CONFIG_PATHOverride pricing file location.
SOLVER_HUB_HTTP_URLOverride hosted Hub HTTP origin for REST calls. Otherwise derived from the WebSocket URL by scheme conversion (wss→https, ws→http) and removal of the terminal /connect. Not a solver.config.json key.
NODE_ENVproduction selects production token data; other values select development data. Use explicit development for testing.
LOG_LEVELdebug, info (default), warn, error, or off; see alerts below.

Pricing

bidding-price.jsonc is the default pricing file. It accepts comments and trailing commas. Values illustrate quoting policy, not recommended fees or expected returns.

FieldRequirement / behavior
defaults.sameCurrencySameChainRequired premium for equal token identifiers on the same chain.
defaults.sameCurrencyDiffChainRequired premium for equal token identifiers across chains.
defaults.diffCurrencySameChainRequired premium for different token identifiers on the same chain.
defaults.diffCurrencyDiffChainRequired premium for different token identifiers across chains.
minFeeRequired nonnegative global fee floor in raw token base units. Use an integer.
routesOptional ordered array of {from:{chain,token},to:{chain,token},premium:{type,value}}. The first matching route overrides category defaults.
depositTimeoutMsOptional global deposit/quote lifetime in milliseconds, at least 30000.
chainDepositTimeoutsOptional record keyed by supported chain ID, values at least 30000 milliseconds; takes precedence over the global timeout.

Every premium is { "type": "rational" | "nominal", "value": <nonnegative number> }. Use integer values: computation converts value and minFee to bigint, so fractional values are not usable even though the configuration schema accepts nonnegative numbers.

“Same currency” means case-insensitive equality of token identifiers, not equality of symbols or an assertion of price parity. Route matching uses exact chain IDs and case-insensitive token identifiers. The reference pricing engine scales token decimals; it does not perform market-price conversion between unrelated assets.

For Order with Order.typ: 1 (fixed input), the fee base is Order.amountIn. For Order.typ: 2 (fixed output), it is Order.amountOut:

  • Rational premium: fee = max(minFee, floor(base × value / 10000)); value: 20 is 20 basis points (0.2%).
  • Nominal premium: fee = max(minFee, value).
  • Fixed input: subtract the fee from input, then scale from input to output decimals.
  • Fixed output: add the fee to output, then scale to input decimals, rounding up.

Nominal values and minFee are raw base units, not USD. The computed pricing fee above is the Reference Solver’s quote premium, not Order.fee, which is Anyflo’s separate fee. A single floor has different economic meaning for tokens with different decimals. With six-decimal input 1000000, 20 bps, minFee: 500, and equal input/output decimals, the pricing fee is 2000 and the fixed-input Bid.payload.quote is 998000 output-token base units. For fixed output, Bid.payload.quote instead specifies input-token base units; see the WebSocket contract. This is arithmetic only, not a live quote. A zero fee, unknown token, fee exhausting fixed input, or nonpositive scaled quote can result in no bid.

This complete route-override fragment can be merged into a pricing file containing all four required defaults and minFee. Replace the illustrative token addresses with your provisioned tokens:

{
  "routes": [
    {
      "from": {
        "chain": "eip155:84532",
        "token": "0x1111111111111111111111111111111111111111"
      },
      "to": {
        "chain": "eip155:421614",
        "token": "0x2222222222222222222222222222222222222222"
      },
      "premium": { "type": "rational", "value": 25 }
    }
  ]
}

The process watches the pricing file. Successful reload logs Hot reloaded bidding config; an invalid edit logs a reload failure and retains the last valid pricing. Confirm successful reload rather than assuming an edit took effect. Main configuration is not hot-reloaded.

Deposit lifetimes are separate from the default 1500 ms bidding window. The effective timeout is the per-chain override, else the global override, else the protocol chain default, constrained to the 30000 ms minimum and chain cap:

Source chainMaximum / default deposit timeout
Base and Base Sepolia; Arbitrum and Arbitrum Sepolia180000 ms
Polygon and Polygon Amoy300000 ms
All supported Sui environments120000 ms
Local eip155:31337600000 ms

Wallets and approvals

Configure both sides of every route: Order.senderChain and Order.recipientChain. The source chain’s Reference Solver setting evmChains[].solverAddress or sui.solverAddress becomes OrderAssignAck.payload.solverAddress, then Order.solverAddress: the address receiving the solver’s source-chain value.

The destination payout signer is not an Order property. The Reference Solver derives it from EVM_<numeric-chain-id>_PRIVATE_KEY or SUI_PRIVATE_KEY for Order.recipientChain. It pays Order.amountOut of Order.tokenOut to Order.recipientAddr, the payout recipient—not the solver’s signing wallet. On EVM, evmChains[].depositAccountAddress identifies the source deposit contract and evmChains[].deliveryAccountAddress the destination delivery contract/token spender; neither identifies the signing wallet.

Maintain gas and sufficient payout-token inventory for concurrent orders on every destination chain. Startup rejects zero gas or supported-token balances, and RPC failures can also block startup. Nonzero is not enough to prove an order is affordable. Bidding balance checks are advisory: another transaction can consume funds before payout. Operators must monitor balances throughout operation.

On EVM, DeliveryAccount.payout() spends the key-derived wallet’s tokens with ERC20 transferFrom. Grant the evmChains[].deliveryAccountAddress configured for Order.recipientChain sufficient allowance from that wallet for each supportedTokens entry. Sui does not use ERC20 approvals.

  • Recommended: runApproveTxs: false. Pre-approve externally, monitor remaining allowances, and replenish before exhaustion. Startup rejects zero EVM allowances in this mode, but does not guarantee sufficient allowance for later orders.
  • runApproveTxs: true. Before each payout, the process reads the allowance and submits an approval increasing it by the payout amount. This adds a transaction, confirmation wait and fees. Concurrent same-token payouts can race and overwrite allowance increases. Tokens requiring a zero-reset before a nonzero approval can reject this behavior. Do not enable it for such tokens.

Source receipts and withdrawal

Direct EVM deposits (Order.directDeposit true) transfer Order.amountIn of Order.tokenIn to Order.solverAddress on Order.senderChain. Indirect deposits (Order.directDeposit: false) credit that address in the source deposit contract’s solverBalances(address solver,address token) accounting instead. Order.fee is the separate Anyflo fee, not part of the solver balance.

Only the credited Order.solverAddress can call the source deposit contract’s withdrawSolverBalance(address token,uint256 amount,address dest) to withdraw its credit. Use an address you can transact from, not a receive-only address. Confirm evmChains[].depositAccountAddress, Order.tokenIn and the credited amount with Anyflo for your environment before withdrawal. The Reference Solver does not automate withdrawals.

Receiving or withdrawing source-chain funds does not automatically replenish a destination chain. Rebalancing inventory, funding gas, and maintaining withdrawal access remain the operator’s responsibility.

Running and persistence

Use a persistent working directory containing operator configuration and SQLite state. After staging provisioning, run:

NODE_ENV=development anyflo-solver run

Use NODE_ENV=production only with confirmed matching production identity, routes, deployments, tokens, wallets and pricing. Changing the environment variable alone does not provision production access. Use an external supervisor such as systemd or your container platform; the application has no in-process supervisor.

Startup loads configuration and secrets, registers monitoring, opens SQLite, applies migrations, and performs wallet/RPC/allowance checks before normal operation. migrate is optional maintenance, not an enrollment step.

Preserve the database and SQLite WAL state across upgrades and restarts. Relative dbPath resolves from the working directory. Back up consistently using SQLite-aware operations or a stopped process; do not copy only a live database while omitting its WAL. An upgrade is not a reason to regenerate authentication keys or discard payout history. Do not run multiple interchangeable processes sharing the same database: execution locks are partly in memory, and state protects against duplicate payments.

Pending-order recovery runs once on startup and periodically, defaulting to every 30000 ms. retryPendingIntervalMs: 0 disables both. Recovery advances stored eligible orders; it is not permission to resubmit every transaction. Failures without a saved payout hash can remain retryable after funding/RPC issues are corrected. A saved hash requires receipt/canonical-chain reconciliation, not a second payout. Preserve that hash even when a local timeout or RPC error made the result uncertain.

Investigate deposit/delivery reorgs and finality before taking manual action. Reconcile indexed notifications, stored payout hashes and on-chain receipts; never delete state to force execution. If suspended, obtain Anyflo reactivation then restart the process; changing keys alone does not reactivate a solver.

Monitoring and alerts

Monitor process lifecycle and Hub connectivity separately from chain RPCs, gas, token balances, EVM allowances, payout receipts and finality/reorgs. A healthy local endpoint does not certify funded wallets or healthy chain RPCs.

The main configuration accepts this alert fragment:

{
  "alert": {
    "sinks": [
      {
        "type": "webhook",
        "url": "https://alerts.example.com/solver",
        "enabled": true
      }
    ]
  }
}
SinkFields
webhooktype, HTTPS url, optional nonempty bearerToken, enabled (default true). Sends JSON batches with an events array.
slackSame transport fields, with type: "slack"; sends Slack-formatted errors.
pagerdutytype: "pagerduty", nonempty routing key, enabled (default true).

Webhook/Slack URLs must use HTTPS and contain no embedded username/password. Store bearer tokens and routing keys securely: configuration JSON does not interpolate ${ENV_VAR} strings. No enabled sinks means no remote alerts.

Slack receives errors broadly; webhook and PagerDuty routing is limited to selected high-priority events: execution failure, insufficient execution balance, startup failure after sink registration, reconnect storms, dangerous deposit reorgs around payout broadcast, and deposit-verification retry timeout. Per-order high-priority duplicates are suppressed using a bounded 10000-entry cache of message/order pairs; eviction means suppression is not guaranteed until restart.

LOG_LEVEL filtering happens before error listeners. off disables alerts as well as logs. Configuration/secret-loading failures occur before sinks are registered, so collect supervisor stdout/stderr independently. Delivery failures or unreachable alert destinations also require independent process/log monitoring. After changing sink configuration, restart and confirm the intended alert channel through your operational test procedure.

Local operator API

This is the Reference Solver’s private, unauthenticated loopback interface, not the hosted SolverHub API and not customer Gateway administration. Default base: http://127.0.0.1:2102. Keep it private; do not expose it directly to a public network.

SOLVER_ADMIN_BASE_URL overrides the bind hostname despite its name; SOLVER_ADMIN_PORT overrides the port. Neither adds authentication. Restrict access through host/network controls when changing the defaults.

Method and pathQuery / successful response
GET /healthAlways JSON; schema below. HTTP 200 when healthy, otherwise 503.
GET /v1/admin/bidsOptional type=active or type=past; array of bid records. Omit for all bids.
GET /v1/admin/orders/activeOrder[]; optional repeated status filters Order.status by selected, accepted, executing, depositNotAck, deliveredNotAck. Omit for all active statuses.
GET /v1/admin/orders/executedOrder[].
GET /v1/admin/orders/failedArray of {order: Order, failedAt: number} payout-failure records; order is the shared Order object.
GET /v1/admin/docsLocal generated API documentation.

Query routes negotiate response encoding; set Accept: application/json for readable JSON. Without it, CBOR may be returned. JSON bigint amounts, including bid quote, are decimal strings. See the complete shared Order data schema rather than treating local records as independent proof of chain execution.

Bid records have exactly these fields:

FieldType
orderIdUUIDv4 string
solverIdNumber
quoteBigint, decimal string in JSON; token base units
revisionCountNumber
createdAtNumber

Health fields:

FieldValues
service"solver"
solverIdPositive integer
statusstarting, healthy, degraded, stopping
uptimeSecondsNonnegative integer
checks.solverstarting, running, stopping
checks.solverHubconnecting, connected, disconnected
curl --fail http://127.0.0.1:2102/health
curl --fail-with-body -H 'Accept: application/json' \
  'http://127.0.0.1:2102/v1/admin/bids?type=active'
curl --fail-with-body -H 'Accept: application/json' \
  'http://127.0.0.1:2102/v1/admin/orders/active?status=accepted&status=executing'
curl --fail-with-body -H 'Accept: application/json' \
  http://127.0.0.1:2102/v1/admin/orders/executed
curl --fail-with-body -H 'Accept: application/json' \
  http://127.0.0.1:2102/v1/admin/orders/failed

Healthy means status: "healthy", checks.solver: "running", and checks.solverHub: "connected"; it makes no wallet/RPC readiness assertion. Empty query arrays before any matching orders are normal. Verify payout hashes on the correct chain as well as in local records.

Troubleshooting

SymptomCheckSafe recovery
Configuration or secret loading failsWorking directory, override paths, JSON/JSONC errors, required fields; SOLVER_AUTH_PRIVATE_KEY JSON {kid,jwk} and configured wallet-key formats.Correct the reported input; restrict secret permissions; restart. Do not overwrite an existing .env with generator output. Inspect supervisor logs because sinks may not yet exist.
Startup gas/token/allowance check or RPC failsConfigured evmChains[].chain / sui.chain, wallet derived from its transaction key, rpcUrl, supportedTokens, and EVM spender evmChains[].deliveryAccountAddress.Restore RPC access; fund gas/payout balances and pre-approve the correct spender. Keep safety margins, then restart. Do not bypass checks by removing a needed route.
Hub HTTP 401 / authentication failureActive registered key, numeric positive kid, assigned solver ID, JWT audience, integer-second times and synchronized clock.Correct provisioning/secret/clock; restart to create a fresh connection token. Use protocol error guidance, not a customer API key.
HTTP 403 or socket close 4002Solver participation status with Anyflo—not Order.status or local health status.Request reactivation, then restart. Rotation does not remove suspension.
Bids skipped or missing acknowledgmentSupported source/destination chains and tokens, pricing reload log, positive viable quote, balances and auction deadline; for custom code, signatures and strict message schema.Fix route/pricing/liquidity/latency issues before another test order; consult SolverHub errors. No bid or best price guarantees selection.
Payout failed, no saved hashFailure logs, retry eligibility, gas/token inventory, allowance and RPC.Correct the cause and allow eligible pending recovery. Confirm no transaction was broadcast before manual action.
Payout failed or uncertain, saved hash existsLocal Order.deliverTx, failed-order records (order.deliverTx), receipt/revert and canonical-chain status on Order.recipientChain.Reconcile with Anyflo and the chain; preserve the hash/state. Do not clear state or blindly submit a second payout.
Deposit finality timeout or reorgSource receipt, canonical block, configured finality tier, RPC tag support and destination payout status.Investigate finality/reorg with Anyflo, especially if payout already broadcast; do not treat notification receipt as verified funding.
Health is healthy but wallets are unfundedIndependent current balances/allowances and RPC monitoring.Fund/rebalance and restore allowances; health only covers process/Hub state.
Alerts missingEnabled sinks, LOG_LEVEL, HTTPS credentials/transport, startup timing, delivery warnings and bounded deduplication.Correct sink settings and restart; retain independent supervisor logs. LOG_LEVEL=off and failures before sink registration cannot produce remote alerts.

For key revocation or loss, follow SolverHub key rotation and recovery. For escalation, collect solver ID, environment, order ID, UTC incident time, chain IDs, transaction hashes and sanitized logs. Never include wallet keys, private JWK fields, registry/alert secrets or JWTs. Do not use a local record, retry timer or reconnect as authorization to pay twice.