Add EvelynLiquidityService to periodically realize proceeds from manually funded EVE/SyrupUSDC Raydium concentrated-liquidity positions. Withdraw liquidity from positions sufficiently below the current market price, exchange available SyrupUSDC for USDC, and send available USDC to a configured recipient.
This is the first, deliberately small version of Evelyn's liquidity service. The owner creates and funds positions manually, for example with 10 EVE each. The service does not create or refill positions. The name remains suitable for later liquidity-management features without introducing generic multi-token strategies now.
Initially, positions funded only with EVE provide no SyrupUSDC buy support. Purchases through the pool, including purchases by the burner, convert position inventory into SyrupUSDC. Until withdrawn, that inventory can convert back into EVE if the price falls. This issue automates realization of sufficiently distant positions.
Follow the burner's separation between its domain service interface and the implementation's NenjimProcess lifecycle. NenjimHub owns startup; do not add lifecycle operations to the domain interface or introduce a service-container framework.
Register an optional evelyn.liquidity.prod process and its dedicated wallet/swap dependencies through the existing Hub/Registry model. The Jupiter service used here must sign with the liquidity wallet, not the burner wallet.
Construct/load this service's deployment configuration and dedicated signer only when the process is selected. Preserve existing process selections and startup ordering; do not activate financial operations through a checked-in example.
Run one sequential iteration at a time, with a configurable fixed delay after completion, for example five minutes. The first iteration follows exactly the same rules as subsequent iterations; there is no special startup strategy.
Position eligibility: percentage below the current pool price
Use the current market price from the configured CLMM pool, expressed as SyrupUSDC per EVE. Do not use the calculated reference price from EvelynStatusService, an EVE/USDT ticker, or an assumed SyrupUSDC/USD parity.
Let:
P = current pool price in SyrupUSDC per EVE
h = configured harvest distance in percent, 0 <= h < 100
U = position's upper price boundary in SyrupUSDC per EVE
withdraw when U <= P * (1 - h / 100)
The entire position range must lie at or below the threshold; testing only its lower boundary is insufficient. Equality qualifies.
Example: with P = 15 and h = 10, the threshold is 13.5. A position spanning 13.0–13.5 qualifies; one spanning 13.5–14.0 does not.
Normalize both market price and position boundaries to the same EVE/SyrupUSDC orientation, including when Raydium's mint A/B ordering is reversed. Use pool/position precision for decisions rather than rounded display prices. Read current on-chain pool state for the decision, reusing existing CLMM state/math support. If a usable positive pool price cannot be read, skip withdrawals for that iteration and still attempt steps 2 and 3.
Three independent steps in each iteration
1. Withdraw from eligible positions without closing them
Discover the CLMM position NFTs owned by the dedicated liquidity wallet and restrict processing to the configured pool.
For each eligible position with positive liquidity, withdraw its complete current liquidity into the liquidity wallet.
Keep the position account, NFT, and price range intact. Do not invoke a close-position operation, burn the NFT, or close its NFT token account. Empty positions must remain available for manual reuse and be skipped on subsequent iterations.
At the qualifying price, principal is expected to be SyrupUSDC. Return any pool-token fees collected by the withdrawal to the same wallet. Any EVE received remains there; this service does not swap, burn, or transfer EVE in v1. Other reward tokens are not part of the proceeds pipeline.
A failure for one position is logged and does not prevent attempts for other independently readable positions or the later steps.
2. Exchange available SyrupUSDC for USDC
Read the wallet's current SyrupUSDC balance when this step begins, including balances left by earlier iterations or deposited manually.
If the balance is absent or at/below the reserve, skip the step. Otherwise swap that full available amount to USDC through the existing exact-input Jupiter service, using an explicit maximum slippage setting.
Leave the configured SyrupUSDC reserve in the wallet. Respect mint precision and never round an amount upward beyond the available balance.
3. Send available USDC to the recipient
Read the wallet's current USDC balance when this step begins, including balances left by earlier iterations or deposited manually.
If the balance is absent or at/below the reserve, skip the step. Otherwise send that full available amount as SPL USDC to the configured recipient wallet, using existing wallet transfer/account-creation support.
Leave the configured USDC reserve in the wallet. The recipient address denotes a wallet owner, not a token-account address.
Failure handling and recovery
The on-chain positions and wallet balances act as the durable queue. Each step reads its own current inputs and can complete independently. For example, an unsuccessful withdrawal must not block exchanging an existing SyrupUSDC balance; an unsuccessful swap must not block sending an existing USDC balance.
On an ordinary operation failure, log it and continue independent work. At the next scheduled iteration, read positions and balances again and attempt whatever remains. This also applies after a restart or an operation whose outcome was unknown when the call returned. Do not introduce a service-level pending-transaction journal, per-withdrawal proceeds attribution, payout identifiers, or a requirement to reconcile an earlier signature before a later iteration can act on current state.
Reuse existing transaction submission/confirmation behavior within each underlying operation. Do not add an immediate retry loop or replay a previously signed transaction in the liquidity service. Shutdown interruption must still stop work normally.
All SyrupUSDC and USDC above the configured reserves in this dedicated wallet are eligible, regardless of their provenance. Logs should identify the step, affected position where applicable, amounts, and any returned transaction signature, without exposing signer secrets.
Configuration and checked-in example
Provide a dedicated versioned configuration file, conf/evelyn-liquidity.conf, and a copy/edit template, conf/evelyn-liquidity.conf.example, beginning with FORMAT_VERSION=1. Follow existing configuration validation conventions. The example is documentation/template input only and must never be loaded as a runtime fallback.
Configuration must cover:
Setting
Meaning / example
Liquidity wallet
Dedicated public Solana address and binding to its existing external signer configuration
CLMM pool
The existing EVE/SyrupUSDC concentrated pool whose owned positions are managed
Harvest distance
Percentage below current pool price; example 10 means 10%
Iteration interval
Positive delay; example 300 seconds
USDC recipient
Destination Solana wallet owner address
Minimum SyrupUSDC reserve
Nonnegative token amount; example must be 0
Minimum USDC reserve
Nonnegative token amount; example must be 0
Maximum swap slippage
Explicit Jupiter limit in basis points
Withdrawal slippage
Explicit limit used to derive CLMM minimum received amounts
The reserves are optional operating preferences: the owner can choose values such as 0.01 SyrupUSDC and 0.5 USDC, or leave either at zero. Do not substitute hidden positive reserves or automatically close emptied source token accounts.
Validate the interval, percentage range, reserve amounts/precision, slippage limits, pool type/token identities, and address/signer binding before the service starts its worker. Resolve EVE, SyrupUSDC, USDC, mint addresses and token programs through the existing canonical currency identity infrastructure. Use public placeholders for deployment-specific addresses/signers in the example; commit no credentials. Document that the wallet needs SOL for transactions and any required token-account creation.
Necessary underlying Raydium support
The current Raydium API already discovers concentrated positions and reads pool/position state and ranges, but exposes no liquidity-withdrawal operation. Add only the support required to build, sign, and execute a full CLMM liquidity decrease while preserving the position NFT. Keep transaction/account details behind the Raydium operation and reuse existing Solana wallet/signing primitives. Apply withdrawal minimum-output limits; do not implement unchecked withdrawals by always setting both minimums to zero.
Withdrawal and close are distinct Raydium actions: the position can remain after liquidity is reduced to zero. See Raydium's liquidity-management documentation and the official CLMM decrease instruction. Verify the actual instruction/accounts required by the configured pool during implementation; this issue does not request a complete Raydium trading API.
Verification and acceptance
Build the main sources and directly affected verification code; add/update OpenSpec capabilities and service/configuration documentation, validate the OpenSpec change, and run git diff --check.
Use controlled dependencies/fixtures to verify threshold equality, just-above-threshold rejection, upper-bound selection, token-order inversion, and exclusion of other pools/owners and zero-liquidity positions.
Verify full liquidity withdrawal retains the NFT/position and uses the expected wallet, pool, liquidity amount and minimum-output protection. Verify any EVE returned is left untouched.
Verify reserve subtraction, missing/zero balances, equality with the reserve, and zero/nonzero reserve settings. For example, 1.01 SyrupUSDC with reserve 0.01 swaps 1.00; 2.5 USDC with reserve 0.5 sends 2.0.
Verify each failure leaves later independent steps usable, subsequent iterations/restarts act on remaining on-chain state, and completed empty positions are skipped. No persistent service queue or pending-signature gate is required.
Verify a market decline does not create or refill positions. Positions that no longer qualify retain their liquidity, even if it is entirely EVE.
Verify iterations do not overlap, shutdown interrupts normally, invalid configuration fails before financial work, and both reserves are exactly zero in the checked-in example.
Verification must not submit live withdrawals, swaps, transfers, or other financial operations.
Scope and related work
No position creation, automatic funding, range changes, rebalancing, position closure, minting/burning, reference-price dependency, Mission Control UI, new notification flow, or general multi-token strategy framework in v1. In particular, 10 EVE per new position is a manual operating example, not a setting used by this service yet.
Related work: #88 (EvelynStatusService naming and reference price), #72 (burner service), #83 (domain/lifecycle separation), #51 (exact-input Jupiter swaps), and #50 (SPL transfers).
## Problem and outcome
Add `EvelynLiquidityService` to periodically realize proceeds from manually funded EVE/SyrupUSDC Raydium concentrated-liquidity positions. Withdraw liquidity from positions sufficiently below the current market price, exchange available SyrupUSDC for USDC, and send available USDC to a configured recipient.
This is the first, deliberately small version of Evelyn's liquidity service. The owner creates and funds positions manually, for example with 10 EVE each. The service does not create or refill positions. The name remains suitable for later liquidity-management features without introducing generic multi-token strategies now.
Initially, positions funded only with EVE provide no SyrupUSDC buy support. Purchases through the pool, including purchases by the burner, convert position inventory into SyrupUSDC. Until withdrawn, that inventory can convert back into EVE if the price falls. This issue automates realization of sufficiently distant positions.
## Service and runtime integration
- Interface: `com.r35157.evelyn.service.liquidity.EvelynLiquidityService`.
- Reference implementation: `com.r35157.evelyn.service.liquidity.impl.ref.EvelynLiquidityServiceImpl`.
- Follow the burner's separation between its domain service interface and the implementation's `NenjimProcess` lifecycle. NenjimHub owns startup; do not add lifecycle operations to the domain interface or introduce a service-container framework.
- Register an optional `evelyn.liquidity.prod` process and its dedicated wallet/swap dependencies through the existing Hub/Registry model. The Jupiter service used here must sign with the liquidity wallet, not the burner wallet.
- Construct/load this service's deployment configuration and dedicated signer only when the process is selected. Preserve existing process selections and startup ordering; do not activate financial operations through a checked-in example.
- Run one sequential iteration at a time, with a configurable fixed delay after completion, for example five minutes. The first iteration follows exactly the same rules as subsequent iterations; there is no special startup strategy.
## Position eligibility: percentage below the current pool price
Use the current market price from the configured CLMM pool, expressed as **SyrupUSDC per EVE**. Do not use the calculated reference price from `EvelynStatusService`, an EVE/USDT ticker, or an assumed SyrupUSDC/USD parity.
Let:
```text
P = current pool price in SyrupUSDC per EVE
h = configured harvest distance in percent, 0 <= h < 100
U = position's upper price boundary in SyrupUSDC per EVE
withdraw when U <= P * (1 - h / 100)
```
The entire position range must lie at or below the threshold; testing only its lower boundary is insufficient. Equality qualifies.
Example: with `P = 15` and `h = 10`, the threshold is `13.5`. A position spanning `13.0–13.5` qualifies; one spanning `13.5–14.0` does not.
Normalize both market price and position boundaries to the same EVE/SyrupUSDC orientation, including when Raydium's mint A/B ordering is reversed. Use pool/position precision for decisions rather than rounded display prices. Read current on-chain pool state for the decision, reusing existing CLMM state/math support. If a usable positive pool price cannot be read, skip withdrawals for that iteration and still attempt steps 2 and 3.
## Three independent steps in each iteration
### 1. Withdraw from eligible positions without closing them
- Discover the CLMM position NFTs owned by the dedicated liquidity wallet and restrict processing to the configured pool.
- For each eligible position with positive liquidity, withdraw its complete current liquidity into the liquidity wallet.
- Keep the position account, NFT, and price range intact. Do not invoke a close-position operation, burn the NFT, or close its NFT token account. Empty positions must remain available for manual reuse and be skipped on subsequent iterations.
- At the qualifying price, principal is expected to be SyrupUSDC. Return any pool-token fees collected by the withdrawal to the same wallet. Any EVE received remains there; this service does not swap, burn, or transfer EVE in v1. Other reward tokens are not part of the proceeds pipeline.
- A failure for one position is logged and does not prevent attempts for other independently readable positions or the later steps.
### 2. Exchange available SyrupUSDC for USDC
- Read the wallet's current SyrupUSDC balance when this step begins, including balances left by earlier iterations or deposited manually.
- Calculate `swapAmount = balance - minimumSyrupUSDCReserve`.
- If the balance is absent or at/below the reserve, skip the step. Otherwise swap that full available amount to USDC through the existing exact-input Jupiter service, using an explicit maximum slippage setting.
- Leave the configured SyrupUSDC reserve in the wallet. Respect mint precision and never round an amount upward beyond the available balance.
### 3. Send available USDC to the recipient
- Read the wallet's current USDC balance when this step begins, including balances left by earlier iterations or deposited manually.
- Calculate `sendAmount = balance - minimumUSDCReserve`.
- If the balance is absent or at/below the reserve, skip the step. Otherwise send that full available amount as SPL USDC to the configured recipient wallet, using existing wallet transfer/account-creation support.
- Leave the configured USDC reserve in the wallet. The recipient address denotes a wallet owner, not a token-account address.
## Failure handling and recovery
The on-chain positions and wallet balances act as the durable queue. Each step reads its own current inputs and can complete independently. For example, an unsuccessful withdrawal must not block exchanging an existing SyrupUSDC balance; an unsuccessful swap must not block sending an existing USDC balance.
On an ordinary operation failure, log it and continue independent work. At the next scheduled iteration, read positions and balances again and attempt whatever remains. This also applies after a restart or an operation whose outcome was unknown when the call returned. Do not introduce a service-level pending-transaction journal, per-withdrawal proceeds attribution, payout identifiers, or a requirement to reconcile an earlier signature before a later iteration can act on current state.
Reuse existing transaction submission/confirmation behavior within each underlying operation. Do not add an immediate retry loop or replay a previously signed transaction in the liquidity service. Shutdown interruption must still stop work normally.
All SyrupUSDC and USDC above the configured reserves in this dedicated wallet are eligible, regardless of their provenance. Logs should identify the step, affected position where applicable, amounts, and any returned transaction signature, without exposing signer secrets.
## Configuration and checked-in example
Provide a dedicated versioned configuration file, `conf/evelyn-liquidity.conf`, and a copy/edit template, `conf/evelyn-liquidity.conf.example`, beginning with `FORMAT_VERSION=1`. Follow existing configuration validation conventions. The example is documentation/template input only and must never be loaded as a runtime fallback.
Configuration must cover:
| Setting | Meaning / example |
| --- | --- |
| Liquidity wallet | Dedicated public Solana address and binding to its existing external signer configuration |
| CLMM pool | The existing EVE/SyrupUSDC concentrated pool whose owned positions are managed |
| Harvest distance | Percentage below current pool price; example `10` means 10% |
| Iteration interval | Positive delay; example 300 seconds |
| USDC recipient | Destination Solana wallet owner address |
| Minimum SyrupUSDC reserve | Nonnegative token amount; **example must be `0`** |
| Minimum USDC reserve | Nonnegative token amount; **example must be `0`** |
| Maximum swap slippage | Explicit Jupiter limit in basis points |
| Withdrawal slippage | Explicit limit used to derive CLMM minimum received amounts |
The reserves are optional operating preferences: the owner can choose values such as `0.01` SyrupUSDC and `0.5` USDC, or leave either at zero. Do not substitute hidden positive reserves or automatically close emptied source token accounts.
Validate the interval, percentage range, reserve amounts/precision, slippage limits, pool type/token identities, and address/signer binding before the service starts its worker. Resolve EVE, SyrupUSDC, USDC, mint addresses and token programs through the existing canonical currency identity infrastructure. Use public placeholders for deployment-specific addresses/signers in the example; commit no credentials. Document that the wallet needs SOL for transactions and any required token-account creation.
## Necessary underlying Raydium support
The current `Raydium` API already discovers concentrated positions and reads pool/position state and ranges, but exposes no liquidity-withdrawal operation. Add only the support required to build, sign, and execute a full CLMM liquidity decrease while preserving the position NFT. Keep transaction/account details behind the Raydium operation and reuse existing Solana wallet/signing primitives. Apply withdrawal minimum-output limits; do not implement unchecked withdrawals by always setting both minimums to zero.
Withdrawal and close are distinct Raydium actions: the position can remain after liquidity is reduced to zero. See [Raydium's liquidity-management documentation](https://github.com/raydium-io/raydium-docs-v1/blob/main/user-flows/add-remove-liquidity.mdx) and [the official CLMM decrease instruction](https://github.com/raydium-io/raydium-clmm/blob/master/programs/amm/src/instructions/decrease_liquidity.rs). Verify the actual instruction/accounts required by the configured pool during implementation; this issue does not request a complete Raydium trading API.
## Verification and acceptance
- Build the main sources and directly affected verification code; add/update OpenSpec capabilities and service/configuration documentation, validate the OpenSpec change, and run `git diff --check`.
- Use controlled dependencies/fixtures to verify threshold equality, just-above-threshold rejection, upper-bound selection, token-order inversion, and exclusion of other pools/owners and zero-liquidity positions.
- Verify full liquidity withdrawal retains the NFT/position and uses the expected wallet, pool, liquidity amount and minimum-output protection. Verify any EVE returned is left untouched.
- Verify reserve subtraction, missing/zero balances, equality with the reserve, and zero/nonzero reserve settings. For example, `1.01` SyrupUSDC with reserve `0.01` swaps `1.00`; `2.5` USDC with reserve `0.5` sends `2.0`.
- Verify each failure leaves later independent steps usable, subsequent iterations/restarts act on remaining on-chain state, and completed empty positions are skipped. No persistent service queue or pending-signature gate is required.
- Verify a market decline does not create or refill positions. Positions that no longer qualify retain their liquidity, even if it is entirely EVE.
- Verify iterations do not overlap, shutdown interrupts normally, invalid configuration fails before financial work, and both reserves are exactly zero in the checked-in example.
- Verification must not submit live withdrawals, swaps, transfers, or other financial operations.
## Scope and related work
No position creation, automatic funding, range changes, rebalancing, position closure, minting/burning, reference-price dependency, Mission Control UI, new notification flow, or general multi-token strategy framework in v1. In particular, 10 EVE per new position is a manual operating example, not a setting used by this service yet.
Related work: #88 (`EvelynStatusService` naming and reference price), #72 (burner service), #83 (domain/lifecycle separation), #51 (exact-input Jupiter swaps), and #50 (SPL transfers).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem and outcome
Add
EvelynLiquidityServiceto periodically realize proceeds from manually funded EVE/SyrupUSDC Raydium concentrated-liquidity positions. Withdraw liquidity from positions sufficiently below the current market price, exchange available SyrupUSDC for USDC, and send available USDC to a configured recipient.This is the first, deliberately small version of Evelyn's liquidity service. The owner creates and funds positions manually, for example with 10 EVE each. The service does not create or refill positions. The name remains suitable for later liquidity-management features without introducing generic multi-token strategies now.
Initially, positions funded only with EVE provide no SyrupUSDC buy support. Purchases through the pool, including purchases by the burner, convert position inventory into SyrupUSDC. Until withdrawn, that inventory can convert back into EVE if the price falls. This issue automates realization of sufficiently distant positions.
Service and runtime integration
com.r35157.evelyn.service.liquidity.EvelynLiquidityService.com.r35157.evelyn.service.liquidity.impl.ref.EvelynLiquidityServiceImpl.NenjimProcesslifecycle. NenjimHub owns startup; do not add lifecycle operations to the domain interface or introduce a service-container framework.evelyn.liquidity.prodprocess and its dedicated wallet/swap dependencies through the existing Hub/Registry model. The Jupiter service used here must sign with the liquidity wallet, not the burner wallet.Position eligibility: percentage below the current pool price
Use the current market price from the configured CLMM pool, expressed as SyrupUSDC per EVE. Do not use the calculated reference price from
EvelynStatusService, an EVE/USDT ticker, or an assumed SyrupUSDC/USD parity.Let:
The entire position range must lie at or below the threshold; testing only its lower boundary is insufficient. Equality qualifies.
Example: with
P = 15andh = 10, the threshold is13.5. A position spanning13.0–13.5qualifies; one spanning13.5–14.0does not.Normalize both market price and position boundaries to the same EVE/SyrupUSDC orientation, including when Raydium's mint A/B ordering is reversed. Use pool/position precision for decisions rather than rounded display prices. Read current on-chain pool state for the decision, reusing existing CLMM state/math support. If a usable positive pool price cannot be read, skip withdrawals for that iteration and still attempt steps 2 and 3.
Three independent steps in each iteration
1. Withdraw from eligible positions without closing them
2. Exchange available SyrupUSDC for USDC
swapAmount = balance - minimumSyrupUSDCReserve.3. Send available USDC to the recipient
sendAmount = balance - minimumUSDCReserve.Failure handling and recovery
The on-chain positions and wallet balances act as the durable queue. Each step reads its own current inputs and can complete independently. For example, an unsuccessful withdrawal must not block exchanging an existing SyrupUSDC balance; an unsuccessful swap must not block sending an existing USDC balance.
On an ordinary operation failure, log it and continue independent work. At the next scheduled iteration, read positions and balances again and attempt whatever remains. This also applies after a restart or an operation whose outcome was unknown when the call returned. Do not introduce a service-level pending-transaction journal, per-withdrawal proceeds attribution, payout identifiers, or a requirement to reconcile an earlier signature before a later iteration can act on current state.
Reuse existing transaction submission/confirmation behavior within each underlying operation. Do not add an immediate retry loop or replay a previously signed transaction in the liquidity service. Shutdown interruption must still stop work normally.
All SyrupUSDC and USDC above the configured reserves in this dedicated wallet are eligible, regardless of their provenance. Logs should identify the step, affected position where applicable, amounts, and any returned transaction signature, without exposing signer secrets.
Configuration and checked-in example
Provide a dedicated versioned configuration file,
conf/evelyn-liquidity.conf, and a copy/edit template,conf/evelyn-liquidity.conf.example, beginning withFORMAT_VERSION=1. Follow existing configuration validation conventions. The example is documentation/template input only and must never be loaded as a runtime fallback.Configuration must cover:
10means 10%00The reserves are optional operating preferences: the owner can choose values such as
0.01SyrupUSDC and0.5USDC, or leave either at zero. Do not substitute hidden positive reserves or automatically close emptied source token accounts.Validate the interval, percentage range, reserve amounts/precision, slippage limits, pool type/token identities, and address/signer binding before the service starts its worker. Resolve EVE, SyrupUSDC, USDC, mint addresses and token programs through the existing canonical currency identity infrastructure. Use public placeholders for deployment-specific addresses/signers in the example; commit no credentials. Document that the wallet needs SOL for transactions and any required token-account creation.
Necessary underlying Raydium support
The current
RaydiumAPI already discovers concentrated positions and reads pool/position state and ranges, but exposes no liquidity-withdrawal operation. Add only the support required to build, sign, and execute a full CLMM liquidity decrease while preserving the position NFT. Keep transaction/account details behind the Raydium operation and reuse existing Solana wallet/signing primitives. Apply withdrawal minimum-output limits; do not implement unchecked withdrawals by always setting both minimums to zero.Withdrawal and close are distinct Raydium actions: the position can remain after liquidity is reduced to zero. See Raydium's liquidity-management documentation and the official CLMM decrease instruction. Verify the actual instruction/accounts required by the configured pool during implementation; this issue does not request a complete Raydium trading API.
Verification and acceptance
git diff --check.1.01SyrupUSDC with reserve0.01swaps1.00;2.5USDC with reserve0.5sends2.0.Scope and related work
No position creation, automatic funding, range changes, rebalancing, position closure, minting/burning, reference-price dependency, Mission Control UI, new notification flow, or general multi-token strategy framework in v1. In particular, 10 EVE per new position is a manual operating example, not a setting used by this service yet.
Related work: #88 (
EvelynStatusServicenaming and reference price), #72 (burner service), #83 (domain/lifecycle separation), #51 (exact-input Jupiter swaps), and #50 (SPL transfers).