spin docs
Strategies

Conditions

How a condition works, the values it can read, and what only JSON can express.

A condition is a rule that is true or false. The bot checks it at a fixed place in the strategy, and goes on only when it is true. A strategy has conditions in these places:

BlockJSONWhen the bot checks it
Buy only ifentry.ifAt once, when the trigger starts the check. No condition: every time.
Then buy if or After the wait, buy ifentry.confirmAfter the wait, or on the answers of the AI steps.
Run this step only ifentry.ai.steps[n].ifBefore an AI step after the first. If it is false, the chain stops there.
Sell when or Ask the AI whenexits[n].ifOn every price change and every second.
Then sell ifexits[n].confirmOn the answers of an AI sell check.

Rows that must all hold

In the workflow, a condition is a list of rows. All rows must hold. Each row has:

  1. A value, for example token.buyers.
  2. Its window, if the value has one, for example 60 seconds.
  3. A comparison.
  4. The number or word to compare with.

Select Add a condition to add the first row, and And also to add more. Select the "x" on a row to remove it. A condition with no rows is "Always": the check goes on every time.

The comparisons are:

In the workflowIn JSONFor
more than>Numbers
at least>=Numbers
less than<Numbers
at most<=Numbers
is==Numbers and words
is not!=Numbers and words
is yesthe value aloneYes or no values
is nonotYes or no values

The yes or no values are token.graduated, token.creator_sold and event.first_buy, and AI answers or webhook fields of the yes or no type. All other rule values are numbers.

In JSON, a value is {"f": "<name>"}, with "args" for its window. A comparison has the value first and the number second. Rows that must all hold are an and:

{
  "and": [
    { ">=": [{ "f": "token.buyers", "args": [60] }, 30] },
    { "not": { "f": "token.creator_sold" } }
  ]
}

Windows

Some values count what happened in a recent window, for example token.buyers(window_s): the distinct wallets that bought in the last window_s seconds. The window is in seconds, and it is at most 3600 (one hour). The engine keeps one hour of trades and prices for each token.

token.price_change(window_s) is the price now divided by the price window_s seconds ago, so 1 is flat and 1.5 is up 50%. Until the engine has watched a pool for that long, it has no value.

Missing values

A value can be unknown, for example token.age_s for a token that launched before the engine started to watch it. A comparison with an unknown value is false. So a rule over missing data never buys and never sells. You do not need to check for missing values.

Prices

token.price is a price in wei per the token's smallest unit. For a token with 18 decimals (every Pons token), that is ETH per whole token.

The workflow shows and takes prices in ETH per whole token when it knows the token's decimals. The JSON keeps the stored number. For a token with fewer decimals, the stored number is larger: ETH per whole token times 10 to the power of (18 minus its decimals). Read prices per whole token.

Where each value can be used

Values come in four groups. Not every group can be used everywhere:

GroupEntry conditionsSell rules
token.*: the token being consideredYesYes
event.*: the trade or launch that started the checkYes, but not with a timer or a webhook triggerNo
position.*: the open positionNoYes
bot.*: the botYesYes

The editor offers only the values that fit where you are. The engine refuses a value that is used where it cannot be known.

The token

The tokenMeaning
token.age_sSeconds since the token launched (null if the launch was before the engine started).
token.pricePrice in wei per the token's smallest unit: ETH per whole token for an 18-decimal token (every Pons token).
token.price_change(window_s)Price now divided by the price window_s seconds ago (1 = flat; at most 3600). Until the engine has watched a pool that long, null.
token.buyers(window_s)Distinct buying wallets in the last window_s seconds.
token.buys(window_s)Number of buys in the last window_s seconds.
token.sells(window_s)Number of sells in the last window_s seconds.
token.buy_eth(window_s)ETH spent on buys in the last window_s seconds.
token.net_eth(window_s)ETH bought minus ETH sold in the last window_s seconds.
token.graduatedPons: true once the token has left its bonding curve for its pool. Torch: true once its price has reached 10 times its launch price (Torch's "Launch Complete"; nothing changes on chain). For a limit of your own, use token.launch_multiple or token.market_cap.
token.launch_multiplePrice now divided by the price at launch (a Torch pool's starting price, a Pons curve's first price). Null if the engine did not see the launch.
token.market_capMarket cap in ETH: price times the 1,000,000,000 supply (Torch tokens; null for others).
token.liquidityETH in the token's market. Real ETH for a Pons curve (not its virtual start), a Torch pool (its preset's ranges), a graduated Pons pool (one full-range position) and a UP or Uniswap v2-style pool. For other UP and Uniswap concentrated pools: the ETH depth near its price (its active liquidity there), which a narrow position can make look deeper than the ETH in it. Null until known.
token.buy_taxThe current buy tax, as a fraction: a Pons curve's, or a Torch token's (with anti-snipe, 0.8 at launch, falling to its own rate over its first 15 to 60 seconds).
token.creator_soldTrue if the creator has sold any of the token (since the engine started watching).

The trade or launch that started the check

The trade or launch that started the checkMeaning
event.ethETH size of the triggering trade.
event.traderWallet that made the triggering trade.
event.first_buyTrue if the triggering trade is the trader's first buy of the token.

The workflow does not offer event.trader. You can use it in JSON.

The position (sell rules only)

The position (exits only)Meaning
position.multiplePrice now divided by the price at entry (before costs).
position.peak_multipleHighest price since entry divided by the price at entry.
position.drawdown_pctPercent below the highest price since entry (for trailing stops).
position.pnl_pctEstimated return if sold now, after the venue fee and tax, ignoring price impact.
position.age_sSeconds since the position opened.

The bot

The botMeaning
bot.open_positionsOpen positions of this bot.

Values from AI answers

Each AI step declares the fields of its answer. A condition after the AI steps can read them as ai.<step>.<field>, for example ai.decide.action. You can use them in Then buy if, in Then sell if, and in Run this step only if of a later step.

A rule can read fields that are a number, yes or no, or one of a list of words. It cannot read text, images or token addresses. A word must be one of the field's own words: the engine refuses "buyy" for a field whose words are buy and skip.

{
  "and": [
    { "==": [{ "f": "ai.decide.action" }, "buy"] },
    { ">=": [{ "f": "ai.decide.confidence" }, 0.7] },
    { "!=": [{ "f": "ai.decide.risk" }, "high"] }
  ]
}

No answer, an answer in the wrong shape, or an answer that comes too late never buys or sells. Read AI steps.

Values from webhook fields

With a webhook trigger, entry conditions can read the fields your service sends as hook.<field>. As with AI answers, a rule can read numbers, yes or no, and words, but not text or token addresses. Read webhooks.

What only JSON can express

The workflow rows cover most conditions. JSON can also express these. The workflow keeps such a condition exactly as it is, and you edit it in the JSON tab.

Or

or holds when at least one of its parts holds. This condition holds when 50 wallets bought in the last minute, or when more than 1 ETH net went in:

{
  "or": [
    { ">=": [{ "f": "token.buyers", "args": [60] }, 50] },
    { ">": [{ "f": "token.net_eth", "args": [60] }, 1] }
  ]
}

Not around more than one value

not around a single yes or no value is a row (is no). not around a larger condition needs JSON. This condition holds unless the token both has graduated and is more than an hour old:

{
  "not": {
    "and": [
      { "f": "token.graduated" },
      { ">": [{ "f": "token.age_s" }, 3600] }
    ]
  }
}

Arithmetic

+, -, * and / combine two values. This condition holds when the token had more than twice as many buys as sells in the last 5 minutes:

{ ">": [{ "f": "token.buys", "args": [300] }, { "*": [{ "f": "token.sells", "args": [300] }, 2] }] }

A division by zero gives an unknown value, so the comparison is false.

A value on the right

The workflow rows put the value first. A comparison with a value on the right, or with a value on both sides, needs JSON.

Limits of a condition

  • An and or an or has 1 to 20 parts.
  • A value takes at most 4 arguments.
  • A word or text is at most 200 characters.
  • There is no null in rules. A missing value already makes a comparison false.

On this page