Opening a NFT card shop in Splinterlands for $25+

in #splinterlands3 years ago (edited)

BUYNANONFT.png


Splinterlands is a wonderful card game that reminds me of early Magic the Gathering. If you are new and like those types of games I highly encourage you to check it out.

Our goal in this series is to add to the ecosystem of this game and have fun. We hope to make money too.

This is meant to be good hearted and fun. Coding is not needed but digging in is encouraged.

The code in this tutorial is good faith but this software is MIT licensed and comes with no warranty or guarantee. You are 100% responsible for anything the script does.

Getting inventory

We are opening a shop, hooray! Let's be smart about it.
If you are a player then buying from the wall is fine. But for us we need to have more control over the price we pay. Market prices tend to fluctuate. If a player is buying a single time mage the player probably doesn't care about a quarter. We don't get such luxuries. If we wait to buy time mage at $0.25 we get twice as many as we would if we bought off the wall at $0.5.

Buying $25 of level 1 time mage

We want to define a json file(bids.json) with our limit orders.

This says to buy 100 time mages at $0.25 for a total of $25(or less).

You can determine the card_detail_id either on splintercards.com or by looking at the url in the splinterlands.com market place.

bids.json

[
    { "card_detail_id": 415, "amount": 100, "bid_usd": "0.25", "edition": 7 }
]

The script

bid.py

from beem import Hive
from beem.transactionbuilder import TransactionBuilder
from beembase.operations import Custom_json
from decimal import Decimal
from urllib.request import urlopen
import json
import os
import time
import argparse

VERSION = "1.03"

def get_collection(owner):
    url = "https://api2.splinterlands.com/cards/collection/"+owner
    return json.loads(urlopen(url).read().decode('utf-8'))

def get_sale_by_card(id, edition, gold=False):
    url = "https://api2.splinterlands.com/market/for_sale_by_card?card_detail_id="+str(id)+"&gold="+str(gold).lower()+"&edition="+str(edition)+"&fee=100"
    return json.loads(urlopen(url).read().decode('utf-8'))

def get_settings():
    url = "https://api2.splinterlands.com/settings"
    return json.loads(urlopen(url).read().decode('utf-8'))

def read_file_or_fail(filename, message):
    if not os.path.exists(filename):
        raise FileNotFoundError(message)
    with open(filename, "r") as f:
        return f.read().rstrip()

def hive_engine_execute_custom_json(json, account):
    hive = Hive(keys=[read_file_or_fail("active.key", "active.key missing")])
    hive.custom_json("sm_market_purchase", json_data=json, required_auths=[account])

def execute_buy(sale, owner, dry_run):
    dec_price = get_settings()['dec_price']
    price_usd = sale["buy_price"]
    price_dec = Decimal(price_usd)/Decimal(dec_price)
    buy = {
            "items": [sale["market_id"]],
            "price": ( "%.3f" % price_dec),
            "currency": "DEC",
            "market": owner,
            "app": "buynano "+VERSION
            }
    if dry_run:
        print("Would buy %d at $%s, %s DEC" % (sale["card_detail_id"], price_usd, price_dec))
        return None
    print("Buying %d at $%s, %s DEC" % (sale["card_detail_id"], price_usd, price_dec))
    return hive_engine_execute_custom_json(buy, owner)

def get_collection_count(collection, card_detail_id, gold):
    count = 0
    for card in collection["cards"]:
        if card["card_detail_id"] == card_detail_id and card["gold"] == gold:
            count += 1
    return count

def cards_needed_to_complete_bid(bid, collection):
    return bid["amount"] - get_collection_count(collection, bid["card_detail_id"], (("gold" in bid) and bid["gold"]))

def should_buy(bid, sale, collection):
    return Decimal(bid["bid_usd"]) >= Decimal(sale["buy_price"]) and cards_needed_to_complete_bid(bid, collection) > 0

def get_sorted_sales(card_detail_id, edition, gold):
    sales = []
    sale_data = get_sale_by_card(card_detail_id, edition, gold)
    for sale in sale_data:
        if sale['xp'] == 1: # level 1 only
            sales.append(sale)
    return sorted(sales, key = lambda sale: Decimal(sale["buy_price"]))

def buy_first(bids, owner, dry_run):
    collection = get_collection(owner)
    for bid in bids:
        for sale in get_sorted_sales(bid["card_detail_id"], bid["edition"], (("gold" in bid) and bid["gold"])):
            if should_buy(bid, sale, collection):
                execute_buy(sale, owner, dry_run)
                return True
        if cards_needed_to_complete_bid(bid, collection) <= 0:
            print("Limit order complete for bid %d, %d @ $%s" % (bid["card_detail_id"], bid["amount"], bid["bid_usd"]))
    return False

parser = argparse.ArgumentParser(description='Splinterlands limit order bid')
parser.add_argument('--dry', action='store_true', help='Do a dry run(do not buy)')
parser.add_argument('owner', type=str, help='Your ign(ex: buynano)')
args = parser.parse_args()

bids = json.loads(read_file_or_fail('bids.json', "Nothing to buy. Add a bids.json"))

while(True):
    if(not buy_first(bids, args.owner, args.dry)):
        print("Nothing found, sleeping.")
        time.sleep(60)
    else:
        time.sleep(1)

This python script will hunt for time mages at all times of the day and attempt to buy any it encounters under or at our bid price.

To explain, we check any dictionary in the bids array for orders. You can think of these as limit orders in an exchange. The NFT market is a sell only market. Anytime a time mage is listed for 0.25 or under you pull the trigger stopping when reaching 100.

Running the script

Install beem

If you have trouble refer to https://pypi.org/project/beem/

> pip install beem

Add active.key

Make a new file called active.key and place your hive active key for the wallet you want to use. This is your private key so don't share it with anyone and make sure it's not network accessible. If on linux you may consider chmod 0400.

Adjust bids.json to set your buy orders

Make sure you set your own buys. You can buy time mages if you want.

Running (dry)

> python bid.py [your ign] --dry

Running (live!)

We're ready to go and we tested! Let's pull the trigger.

> python bid.py [your ign]

Areas for improvement

We currently do not do bulk buys and we could improve efficiency. Additionally we could detect if we are out of DEC. We could add support for higher xp levels. This code was purposefully kept simple.

Using peakmonsters

These two links are really useful to see what happened(use your ign):

Liquidity

There is another issue here. Getting money in. The pairings are DEC pairings. DEC is currently unstable. Putting our $100 into DEC and having it drop to $50 would be problematic. For now we can either feed it manually on-demand or sit in credits.

Credits do not scale for larger budgets. For larger budgets what we want is on-demand liquidity.

If you have a larger budget and need on-demand liquidity contact me for consulting. I have something that works very well here.

About the author

buynano is a nickname pronounced 'boy-nano'(e.g. what's up boooyyynano!?!?').

buynano should not be confused with the cryptocurrency Nano(ticker XNO), which is a feeless, instant, and eco-friendly digital cash solution that can scale to the needs of the entire world.

Legal notice

buynano is not responsible for your monetary gains or losses and none of this constitutes financial advice.


Did you like this content? Comment, like, ask questions, report bugs below. If enough people are interested it will motivate me to continue this series.

Sort:  

Script update in article. Link for convenience:

https://www.codepile.net/pile/KLN8xqGW

Update 1.03 changelog:

  • Improvement: Move bids to bids.json
  • Quality of life: Output when limit order is filled
  • Quality of life: Move owner to command line argument
  • Quality of life: Add --dry flag for testing
  • General refactoring for clarity
  • Better usage of hive engine/beem(thanks again @bauloewe)

Update 1.02 changelog:

  • Fix: Buy check. We were missing an important Decimal cast! Please upgrade(thanks @doominion-spy25)
  • Add support for optional "gold": True (requested)

Update 1.01 changelog:

  • Security: Recommended to use active.key instead of owner.key (thanks @bauloewe)
  • fix time mage default bid to $25 instead of $50

Hey buynano,

interesting tutorial, was also looking into buying card sets with python right now. There's just two thing which are puzzling me a bit:

1

Add owner.key
Make a new file called owner.key and place your hive owner key for the wallet you want to use. This is your private key so don't share it with anyone and make sure it's not network accessible.

By owner key, do you mean the private owner key of the account ? Or the Private posting key ? Because for neither operation you need the owner key. Active and posting are more than enough.

2 Is there a reason you are using the transaction builder instead of the custom json function on the hive instance itself ?

Thanks, good questions.

  1. You might need the active key? I just used owner because I'm lazy :)

  2. Hey there's a custom_json function https://beem.readthedocs.io/en/latest/beem.blockchaininstance.html#beem.blockchaininstance.BlockChainInstance.custom_json

I haven't tried this either but it looks legit. I was scared of messing up those transactions so I went straight API. I bet custom_json would work.

Edit: After thinking about it, (1) could be considered security issue so I issued an update.

about 1. yes, you absolutely shouldn't use the owner key unless necessary. its exposing the account to a small but unnecessary risk. unless someone gets a hold of your script and you stored it in the script itself. then your account is lost. since you can change the master password with your owner key. and with that all the other private keys.

about 2. it does essentially exactly what you are doing if you look at the beem source code. for examples just take a look at some of my tutorials. the lastest about submitting battles uses a lot of custom json stuff, the rental tutorial as well.

but essentially it works like this (you can load the keys through various other means, that is just the easiest for explaining):
hive: Hive = Hive(keys=[posting_privatekey,active_privatekey])

hive_id: str = "sm_stake_tokens"
request = {"token": "SPS", "qty": qty}
hive.custom_json(hive_id, json_data=request, required_posting_auths=["user"])

Nice hive.custom_json worked first try after reading the relevant portion of your automated rental tutorial.

As an aside, cool ai bot! I noticed the gpt-neox-20b(goose.ai) is available, it might be able to help in selecting/summarizing/rating content.

Great :) and you're talking about my curation bot ? yeah it might help. But right now i don't have any further plans for it. The bot was just based on simple latent semantic analysis and a random forest. Nothing too fancy.

Currently busy with splinterlands :)

Where can I find the tutorials you mentioned "for examples just take a look at some of my tutorials. the lastest about submitting battles uses a lot of custom json stuff, the rental tutorial as well."

hi @doominion, just click on my profile. they are all on my hive blog :)

Doeas Peakmonster Bid feature will not do this job?

Several issues led me to choose this strategy over peakmonster bids:

  • Hidden buy walls
  • Low DEC margin, this script requires DEC at time-of-execution instead of time-of-bid
  • On conflict peakmonster bid system has to race us for the inventory. You'll probably win if you aren't watching too many cards.

Congratulations @buynano! You have completed the following achievement on the Hive blockchain and have been rewarded with new badge(s):

You received more than 10 upvotes.
Your next target is to reach 50 upvotes.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

Check out the last post from @hivebuzz:

Hive Power Up Month - Feedback from February day 5
Hive Power Up Month challenge - Feedback from February day 3
Our Hive Power delegations to the last Power Up Month challenge Winners
Support the HiveBuzz project. Vote for our proposal!

What is this?
key = try_load_key("active.key", "owner.key")
Do you need both keys?

Hi, you only need one key. Use active.key.

Ok, I tried
key = try_load_key("active.key")

and this
bids = [
{ "card_detail_id": 439, "amount": 2, "bid_usd": "6.00", "edition": 7 }
]
and this change
def get_sale_by_card(id, edition, gold=True):

Is generating this output which is not what I was expecting.
Buying 439 at $1500, 455746.9692826542645971893066 DEC

Sorry about that - I've updated the script to support gold cards and fix this bug you encountered. Just include "gold": True in the bid after updating.

e.g.
{ "card_detail_id": 439, "amount": 2, "bid_usd": "6.00", "edition": 7, "gold": True }

Edit: code

Sorry I am python noob and not very good at debugging it. I updated to the latest version but I get the following error while trying a dry run (nice option). I suspect card_detail_id and edition are both integers while gold is a string this is a problem. Also I had to tweak the bid json to "gold":"True" (quotes around True)

Traceback (most recent call last):
File "/home/ubuntu/splinterlands/beem/NFTCardShop.py", line 93, in
if(not buy_first(bids, args.owner, args.dry)):
File "/home/ubuntu/splinterlands/beem/NFTCardShop.py", line 77, in buy_first
for sale in get_sorted_sales(bid["card_detail_id"], bid["edition"], bid["gold"]):
TypeError: string indices must be integers

ps - doominion-spy ran out of RC and couldnt post so I had to switch accounts

Hmm. Well "gold" should be just the symbol true, not the string "True".

Could it be that your bids.json is not an array? Happy to help debug it if it's something else too.

bids.json

[
{ "card_detail_id": 439, "amount": 2, "bid_usd": "6.00", "edition": 7, "gold": True }
]

Edit: code

Where did you get that message?