Webhooks
Let your own service start a Legendary strategy with a signed call to the Spinner's webhook.
A webhook trigger lets your own service start a strategy. Your service watches what you want (a stream of posts, a chart alert, a Telegram group) and sends a signed call to the Spinner's webhook. The bot then runs the strategy's entry with the fields your service sent.
A webhook trigger needs a Legendary Spinner.
A call can only start an entry. It cannot move or withdraw funds. Every buy still goes through the strategy's rules, its limits and the live trading limits on the Spinner's guard.
Set up a webhook
Choose the trigger
In the workflow editor, open the When block. In Start when, choose My own service calls its webhook (Legendary).
Declare the fields
Add the fields that your service sends, with Answer field and Holds, as for an AI step's answer. See fields.
Choose what it does
Choose the outcome. To buy a token that your service names, set The token to buy to "The token your service sends". To buy one fixed token, choose it. To launch a token, see launch from a webhook.
Save, and make a secret
Click Save as new version. On the Strategy tab, the Webhook card now shows the webhook's address. Click Make a secret. Copy the secret and keep it in your service's settings, never in its code or a repository. The card shows it only this once.
Sign and send calls
Your service signs each call with the secret. See signing a call.
The Spinner must be activated. Calls are refused until you make a secret.
Fields
A webhook declares 1 to 10 fields. A field name is lowercase letters, digits and _, starting with a letter, up to 32 characters. The types are those of an AI step's answer, but never an image.
| In JSON | Your service sends | Rules read it |
|---|---|---|
"number" | A number | Yes |
{ "number": [0, 1] } | A number in the range | Yes |
"boolean" | true or false | Yes |
["buy", "skip"] | One of the listed words | Yes |
"text" | Text, up to 4,000 characters | No |
"token" | A token's address (0x and 40 hex digits), or null | No |
The strategy uses the fields so:
- Rules read a field as
hook.<field>, for examplehook.score. - Prompts of AI steps, and a launch's fields, include a field as
{{hook.<field>}}. - The token to buy can be a
tokenfield:"target": { "token": "{{hook.token}}" }.
A webhook has no trigger event, so trigger values are unknown in its rules. Token and bot values work as usual.
What the bot does with a call
- Spin's servers check the call (see what a call must pass) and answer 202.
- Within seconds, the bot starts the entry with the call's fields.
- If the strategy buys a token that the call names, the bot looks up the token's market: a Pons or Torch token on its launchpad, any other token in its pools. If no market appears within a minute, it buys nothing.
- The entry runs as any other: Buy only if, any Wait, any AI steps, Then buy if, then the buy. The strategy's limits apply, for example Once per token.
A call that is more than 5 minutes old when the bot gets to it is dropped. A signal that late is no signal.
Example strategy
Your service sends a token and a score from 0 to 1. The bot buys only if the score is at least 0.7 and the token's market has at least 3 ETH. This is an illustration to learn from, not a strategy that is known to make money.
{
"schema": 1,
"name": "My signal service",
"entry": {
"trigger": { "on": "webhook", "fields": { "token": "token", "score": { "number": [0, 1] } } },
"target": { "token": "{{hook.token}}" },
"if": {
"and": [
{ ">=": [{ "f": "hook.score" }, 0.7] },
{ ">=": [{ "f": "token.liquidity" }, 3] }
]
},
"buy_eth": 0.005
},
"exits": [
{ "if": { ">=": [{ "f": "position.multiple" }, 2] }, "sell_pct": 50, "reason": "take_profit" },
{ "if": { "<=": [{ "f": "position.pnl_pct" }, -30] }, "reason": "stop_loss" },
{ "if": { ">=": [{ "f": "position.age_s" }, 7200] }, "reason": "time" }
],
"limits": { "once_per_token": true, "max_open": 3 }
}A webhook strategy needs entry.target (a token, or the one the call names) or entry.launch.
Signing a call
Send a POST to the webhook's address, https://spin.trading/api/hooks/<Spinner number>. The Webhook card shows the exact address. The body is a JSON object with the declared fields. Add these headers:
| Header | Value |
|---|---|
content-type | application/json |
x-spin-timestamp | The time of signing, in Unix seconds. |
x-spin-signature | v1= and the hex HMAC-SHA256 of <timestamp>.<body>, with the secret as the key. |
x-spin-event-id | Optional. Your own id for the event: 1 to 100 letters, digits or . _ : -. |
The signature covers the exact bytes of the body. Sign the body as you send it, and do not format it again after you sign it.
If you send the same event id again, Spin takes the call once only. Without an event id, the signature names the call, so the same signed call sent twice is taken once.
SECRET="whsec_..." # from the Webhook card
BODY='{"token":"0x0000000000000000000000000000000000000001","score":0.9}'
TS=$(date +%s)
SIG="v1=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')"
curl -X POST "https://spin.trading/api/hooks/123" \
-H "content-type: application/json" \
-H "x-spin-timestamp: $TS" \
-H "x-spin-signature: $SIG" \
-H "x-spin-event-id: post-1849" \
--data "$BODY"What a call must pass
Spin checks each call in this order:
- The body is at most 8,192 bytes.
- The Spinner is activated, and its strategy starts on a webhook.
- The Spinner has a secret, made by the wallet that holds the Spinner now.
- The timestamp is within 5 minutes of now.
- The signature matches the timestamp and the exact body.
- The body is JSON, with every declared field, of the declared type, and no other fields.
- The event id, if you send one, has the right form.
- The Spinner has had fewer than 30 calls in the last minute and fewer than 2,000 in the last day.
| Answer | Meaning |
|---|---|
| 202 | Accepted. The body is {"accepted": true, "duplicate": false}. "duplicate": true means Spin took this event before, and takes it once only. |
| 400 | The body is not JSON, a field is missing or of the wrong type, the body has a field that the strategy does not declare, or the event id has the wrong form. |
| 401 | No secret yet, a timestamp more than 5 minutes from now, or a signature that does not match. |
| 404 | The Spinner is not activated, or its strategy does not start on a webhook. |
| 413 | The body is over 8,192 bytes. |
| 429 | Over 30 calls in a minute, or over 2,000 in a day. |
A refused call's body has an error field that says why, for example "score" must be a number from 0 to 1.
The secret
- Spin makes the secret:
whsec_and 32 random bytes. Spin keeps it sealed (encrypted), and shows it only once. - Make a new secret ends the old one at once. Use it if the secret leaks.
- A sale of the Spinner ends its secret. The new holder makes their own.
- The timestamp window means a captured call cannot be sent again later.
Launch from a webhook
A webhook strategy can launch a token instead of buying one. Then your service can write the token itself: the launch fields can be webhook fields, and no AI step is needed.
{
"schema": 1,
"name": "Launch from my service",
"entry": {
"trigger": { "on": "webhook", "fields": { "name": "text", "symbol": "text", "description": "text", "image": "text" } },
"launch": {
"launchpad": "torch",
"name": "{{hook.name}}",
"symbol": "{{hook.symbol}}",
"description": "{{hook.description}}",
"image": "{{hook.image}}"
}
},
"exits": []
}The image field must be an https:// or ipfs:// link to the image. See Launching tokens for the launch fields and their terms.
Webhooks cannot be backtested
What your service would have sent on past days cannot be known, so a webhook strategy cannot be backtested. Run it on paper first.