Automating the Sale of STORJ – Is It Worth It?

I can’t, I’m not a lawyer, especially in the US. It’s just common sense. Another example is how Storj migrated from SJCX to STORJ. As far as I understand, you can’t just abandon already issued tokens; there are rules and requirements (which I’m not aware of).
I don’t know about your country, but issuing, say, gift cards, which are the closest equivalent to tokens in my opinion, and then terminating their service without an expiration date is considered fraudulent in almost all countries.

In the case of Polygon, tokens weren’t created. You basically burned Ethereum tokens in exchange for Polygon tokens. The number of tokens didn’t increase; they remained exactly the same.
The same mechanism doesn’t work when changing payouts. You don’t burn existing tokens, but acquire new ones (in addition). This also disrupts the ecosystem: if a token isn’t used by at least two parties—the buyer and the supplier—they become meaningless. So, to change a token, you need to burn an equivalent number of the original tokens. In this case (since we do not own a favorite token of Jammerdan) - the full loss.
However, I repeat—the problem of changing the payout method doesn’t even exist. We’re just treading water. Until there’s at least an economic need, nothing will change.

No, it requires much more efforts. Not all of them are technical. I would say most of them (99.(9)%) are not technical.

1 Like

You’re absolutely right. Many jurisdictions have much stricter attitudes toward them, and in Europe, they’re practically illegal.

1 Like

The discussions here, even though they aren’t followed by a change, are usefull just to get the whole picture why is this and not that, explore solutions and calm the spirits. Many of us SNO are not so expirienced in running a business that pays parteners in crypto across the entire world, we only see from our end.

1 Like

Yes, I was in those shoes too. Until I was lucky enough to start my own business. Then I discovered many surprising (and painful) truths.

1 Like

A post was merged into an existing topic: Open discussion / ideas for updated tokenomics

In EU we can use USDC and EURC, and maybe oyhers. I know USDT is forbidden.
For stablecoin payouts I’m thinking at one more variant, that dosen’t take into account clients participation.
Offer SNOs the stablecoin payout as an opt-in option, along with the current token, but with some drawbacks:

  • 5-10% payout reduction, that will cover the transfer fees and pay for the extra paperwork;
    or
  • payout transfers only if a threshold of 50$ is bypassed;
    or
  • payout transfers only once per 3 months.

If the majority of SNOs opt in, then we can consider ditching the token entirely, or it can remain as a second option forever.
If there is no serious adoption of the stablecoin payout in a year, then we can ditch it and settle this debate forever.

Prior to this, there should be a poll sent by email to all SNOs, to choose 5 of the top 10-15 stablecoins which they preffer to be payed in.
Then, Storj chooses 1 or 2 or 3 coins that are the most accepted.

Hmmm… you’re making me consider lots of options…

If Storj reaches profitability this year (so they can afford to consider alternatives where they don’t have to rely on their pile of treasury tokens)… they could use the option of stables to roll-out lower payout rates. Like say if a SNO continues to choose STORJ payouts then they get the regular $1.50/TB/m. But… if a SNO wants stables… then only offer $1.25/TB/m?

Right now they can’t afford payouts that don’t use STORJ. But perhaps switching to lower payouts could make different payments happen sooner? :face_with_raised_eyebrow:

If I were Storj, and the current payout system was failing or causing losses, I’d likely choose a payment provider rather than handle stablecoins myself, especially considering I’m unfamiliar with the laws of the countries where my contractors operate. Especially considering they operate in 124 countries.
However, the current system has been working smoothly since 2014. Why should I, as a businessman, change anything?
What will this give me (well, besides additional problems, higher expenses than before, and God forbid - problems with the law)?

Yeah, I’m so thankful the current system has worked so well for so long. And every month more and more nodes come online chasing STORJ payouts! :money_mouth_face: Don’t consider changes until a) the company is in-the-black and b) 100% of payout coins have to be bought-from-market (and the treasury is bare).

Basically squeeze every penny from tokens in the corporate inventory… then (maybe) pivot :wink:

2 Likes

I just send out ideeas for when the time comes, if ever, to not forget them.
Thinking about stables, it seems they are under
more gov scrutiny than common tokens, and they “benefit” of more regulations than others. Central banks put pressure on govs to knock them more often even than the big coins, like BTC, ETH etc.
So yeah, starting dealing with them could be a pain. And you have less certenty long therm.
Storj token is so small that nobody cares to regulate it. It’s survival only depends on daily trading volume, CEXes and DEXes not to delist it.

Not fully true. Storj takes effort to make sure token is used in a way that would not make it fall under specific categories like securities or commodities, which is already a form of regulation. Some examples: 1, 2.

2 Likes

Am I doing it right? :slight_smile:

4 Likes

2 weeks later…: “Coinbase: 100$ were exchanged for 2000 Storj”. :smiling_face_with_sunglasses:

It’s just statistics. Storj in 12m performance: -69%.
-9.6 per month (compound). More you wait more you’ll lose.

Things are going to change? A massive buy pressure incoming? Probably and probably not. Gambling or not gambling? up to you guys :slight_smile:

:joy:

Here is my realised loss of today:

Around 3am today I started to try to sell. I won’t go into details, but it did not work out and required some detour. This took almost exactly 12 hours. 12 hours in which the price follwed only one direction: South.

These crypto payouts as they are are a complete joke.

1 Like

This is a skill issue :squinting_face_with_tongue: . You choose where to send your payouts, and where to sell, and if you market-sell or try to manage an asking price. And you made choices that meant it took 12h to get your money.

@arrogantrabbit made different choices and had USD in 2 minutes. And it’s not a Coinbase thing: crypto.com is just as fast!

But back to OPs point: it would be cool to automate selling: maybe scripting the DEX/CEX API? I’m not really motivated enough to learn… manually selling payouts a dozen times a year isn’t a burden - but I like the idea!

(Edit: maybe it’s straightforward: it looks like CDC will run a bot for you to sell your total STORJ position, at market, every hour. So on average you’d sell within around 30min of getting a payout? That would be fast enough for me)

No, it is a systemic issue that requires to sell within 5 minutes after receiving the funds.

This is the root cause. Not the attempts to work around that.

1 Like

But you see, it’s now [checks time] 2026, so you don’t have to learn something to do something. We have robots.

This is what the robot produced during my 20 min commute to work this morning, over the voice (!) chat. I haven’t tested this, but I’m sure with minor massaging it can be made to work.

I will maybe even try it over the weekend and report :smiley:

:warning: BELOW IS AN UNADULTERATED AND UNAUDITED AI VOIMT :WARNING: USE IT AT YOUR OWN RISK :SKULL: NO HUMAN MEANINGFULLY REVIEWED THIS

Below is the complete deployment guide for an automated Storj → USD market sell on Coinbase, including the single-file Python script, webhook setup, and optional Caddy HTTPS termination.


Storj Auto-Sell to USD — Complete Guide

Overview

You will:

  1. Run a Python webhook listener that:

    • Receives Coinbase on-chain deposit notifications
    • Verifies webhook signatures
    • Immediately places a market sell order via Coinbase Advanced Trade API
  2. Expose it to the internet via valid HTTPS

    • Either with Cloudflare Tunnel (already solved for you)
    • Or with Caddy + Let’s Encrypt (covered here)

Requirements

  • Public HTTPS endpoint for webhook (Cloudflare or Caddy)
  • Coinbase account with API keys (with trade permission)
  • Python 3.9+ installed

Environment Variables

COINBASE_API_KEY
COINBASE_API_SECRET
WEBHOOK_LISTENER_SECRET    # from subscription creation
STORJ_CONTRACT_ADDRESS     # STORJ ERC-20 contract
WATCHED_WALLET_ADDRESS     # your deposit address
WEBHOOK_HOST               # https://your.domain

Set these before running the Python script.


Single Python Script — storj_autosell.py

#!/usr/local/bin/python3

import os, sys, time, json, uuid, hmac, hashlib, base64, logging
from typing import Optional

import requests
from flask import Flask, request, abort

# ───────────────────────── Logging ─────────────────────────

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)

log = logging.getLogger("storj-autosell")

# ───────────────────────── Environment ─────────────────────

required_env = [
    "COINBASE_API_KEY",
    "COINBASE_API_SECRET",
    "WEBHOOK_LISTENER_SECRET",
    "STORJ_CONTRACT_ADDRESS",
    "WATCHED_WALLET_ADDRESS",
    "WEBHOOK_HOST",
]

missing = [e for e in required_env if not os.environ.get(e)]
if missing:
    log.critical("missing env vars: %s", missing)
    sys.exit(1)

API_KEY = os.environ["COINBASE_API_KEY"]
API_SECRET = os.environ["COINBASE_API_SECRET"]
WEBHOOK_SECRET = os.environ["WEBHOOK_LISTENER_SECRET"]
STORJ_CONTRACT = os.environ["STORJ_CONTRACT_ADDRESS"]
WATCHED_ADDR = os.environ["WATCHED_WALLET_ADDRESS"]
HOST_URL = os.environ["WEBHOOK_HOST"]

CDP_BASE = "https://api.cdp.coinbase.com"
ADV_BASE = "https://api.coinbase.com"

HTTP_TIMEOUT = 10
MAX_RETRIES = 5

processed_tx = set()   # prevents duplicate sells (in-memory)

# ───────────────────────── Coinbase Signing ────────────────

def sign_request(method: str, path: str, body: str = ""):
    ts = str(int(time.time()))
    prehash = ts + method + path + body
    sig = base64.b64encode(
        hmac.new(API_SECRET.encode(), prehash.encode(), hashlib.sha256).digest()
    ).decode()

    return {
        "CB-ACCESS-KEY": API_KEY,
        "CB-ACCESS-SIGN": sig,
        "CB-ACCESS-TIMESTAMP": ts,
        "Content-Type": "application/json"
    }

# ───────────────────────── Retry Wrapper ───────────────────

def post_with_retry(url, headers, body) -> Optional[requests.Response]:
    for attempt in range(MAX_RETRIES):
        try:
            r = requests.post(
                url,
                headers=headers,
                data=body,
                timeout=HTTP_TIMEOUT,
            )

            if r.status_code < 500:
                return r

            log.warning("server error %s — retrying", r.status_code)

        except requests.RequestException as e:
            log.warning("request failure: %s", e)

        time.sleep(2 ** attempt)

    log.error("max retries exceeded for %s", url)
    return None

# ───────────────────────── Webhook Subscription ────────────

def create_subscription():
    path = "/platform/v2/data/webhooks/subscriptions"

    body = json.dumps({
        "description": "Storj deposit watcher",
        "eventTypes": ["onchain.activity.detected"],
        "target": {"url": f"{HOST_URL}/webhook", "method": "POST"},
        "labels": {
            "contract_address": STORJ_CONTRACT,
            "event_name": "Transfer"
        },
        "isEnabled": True
    })

    headers = sign_request("POST", path, body)

    r = post_with_retry(CDP_BASE + path, headers, body)

    if not r:
        log.error("failed to create webhook subscription")
        return

    log.info("subscription response %s %s", r.status_code, r.text)

# ───────────────────────── Market Sell ─────────────────────

def place_market_sell():

    path = "/api/v3/brokerage/orders"

    body_dict = {
        "client_order_id": f"sell-storj-{uuid.uuid4()}",
        "product_id": "STORJ-USD",
        "side": "SELL",
        "order_configuration": {"market_market_ioc": {}}
    }

    body = json.dumps(body_dict)
    headers = sign_request("POST", path, body)

    r = post_with_retry(ADV_BASE + path, headers, body)

    if not r:
        log.error("order submission failed (no response)")
        return None

    try:
        payload = r.json()
    except Exception:
        log.error("non-json order response: %s", r.text)
        return None

    if r.status_code >= 300:
        log.error("order rejected: %s", payload)
        return None

    log.info("order accepted: %s", payload)
    return payload

# ───────────────────────── Webhook Listener ────────────────

app = Flask(__name__)

def verify_sig(sig, payload):
    computed = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(computed, sig)

@app.route("/webhook", methods=["POST"])
def webhook():

    payload = request.get_data()
    sig = request.headers.get("CB-SIGNATURE-SHA256")

    if not sig or not verify_sig(sig, payload):
        log.warning("signature verification failed")
        abort(400)

    try:
        evt = request.json
    except Exception:
        log.warning("invalid json payload")
        abort(400)

    activity = evt.get("activity") or {}

    tx_id = activity.get("transactionHash")

    # Idempotency guard
    if tx_id and tx_id in processed_tx:
        log.info("duplicate event ignored: %s", tx_id)
        return {"status": "duplicate"}, 200

    if (
        activity.get("to") == WATCHED_ADDR and
        activity.get("symbol") == "STORJ"
    ):
        log.info("deposit detected: %s", activity)

        result = place_market_sell()

        if result:
            if tx_id:
                processed_tx.add(tx_id)
            return {"status": "sold"}, 200

        log.error("sell failed")
        return {"status": "error"}, 500

    return {"status": "ignored"}, 200

# ───────────────────────── Startup ─────────────────────────

if __name__ == "__main__":

    try:
        create_subscription()
    except Exception as e:
        log.error("subscription creation crashed: %s", e)

    log.info("starting webhook listener on :8000")

    try:
        app.run(host="0.0.0.0", port=8000)
    except Exception as e:
        log.critical("flask crashed: %s", e)
        raise

HTTPS Termination Options

If you already use Cloudflare Tunnel

  • Use your Tunnel’s public hostname as WEBHOOK_HOST
  • No local TLS needed
  • Ensure Tunnel forwards /webhook to localhost:8000

Coinbase webhook target → HTTPS Cloudflare → local Flask HTTP.


Optional: Caddy HTTPS Termination

If you do not have Cloudflare Tunnel and need public HTTPS:

  1. Install Caddy
pkg install caddy
  1. Caddyfile
    Place at /usr/local/etc/caddy/Caddyfile:
storj-autosell.example.com {
    reverse_proxy 127.0.0.1:8000
}
  • Replace storj-autosell.example.com with your DNS name.
  • Caddy will automatically obtain and renew Let’s Encrypt certificates.
  1. Start Caddy
service caddy start
  1. Set:
WEBHOOK_HOST=https://storj-autosell.example.com

before running the Python script.


Steps to Deploy

  1. Set up HTTPS (Cloudflare Tunnel or Caddy)

  2. Set environment variables

  3. Run:

    python3 storj_autosell.py
    
  4. Coinbase will deliver webhook events to:

    https://<WEBHOOK_HOST>/webhook
    
  5. On deposit, script places a market sell order.


Notes

  • You must update WEBHOOK_LISTENER_SECRET after subscription creation.
  • Coinbase API requires trade permissions for order placement.
  • Coinbase will deliver only to valid public HTTPS endpoints.
2 Likes

Are you one of the cool kids with an OpenClaw/MiniMax setup at home? That tech is changing so fast…

Nah. I’ve tried going that route, but paying for API calls is not only cheaper but also results arrive much faster. The amount of compute power these company have access to is astounding.

But it was nice justification to buy myself a beefy Mac: “yes, it’s expensive, but I’ll save tons of money on LLM API calls by running models locally.”.. I suspected I could have been lying to myself, but I really wanted a reason to buy a beefy Mac :smiley:

So in summary, I’d say I’m an old fart with an OpenAI subscription, and obsessively scrutinized and very restrictive system prompt.

As an assist for serious software development, however, Anthropic Opus 4 via Claude code is unbeatable as of today. Not current sonnet, opus specifically. It’s at the point where it actually helps, as opposed to gets in the way with goofy approaches. But I digress.

2 Likes