Programming Tutorial: Renting your splinterlands card collection with python

in #splinterlands3 years ago (edited)

Splinterlands offers plenty of opportunity to make money. Aside from playing the game, there's passive investing and active trading.

But there's one more option. Renting. Let's say you have a card collection, but no time to play or are speculating on higher prices.
In that case you can rent your collection out.

Once you looked into it, renting can be quite the time consuming task. Making sure that all cards are rented out, adjusting prices and canceling unprofitable rentals.

Wouldn't it be nice if you could automate all of that? Well, fret not it's possible. And not as hard as it looks.

Requirements

DependencyVersionSource
python3.xhttps://www.python.org/downloads/
Beem0.24.26(or latest)https://pypi.org/project/beem/

1. Making Inventory

First we need to know which cards we want to rent out. Usually it's not the whole collection but a subset. To uniquely identify a card we need its unique identifier uid for short. The uid can be found next to your card when viewing your card collection. Check the column "Card ID"

image.png

For this tutorial I'll be using two cards. The giant roc from above and a untamed card. You'll see later why I'm using two cards from different generations.

  1. C1-2-M57QKW5M68 (lvl 10 giant roc beta card)
  2. C4-209-F0UTJ8IC5S (lvl 4 chain golem untamed card)

1.1 Loading your collection


But now we need a couple informations about those cards. So lets write our first function to get those. First lets get your card collection:

import requests

API2 = "https://api2.splinterlands.com"


def get_card_collection(player):
    url: str = API2 + "/cards/collection/" + player
    return requests.get(url).json()["cards"]

As you can see, the function is rather simple, it just takes the player name and calls the endpoint.

The endpoint returns a list objects with lots of details for each of your cards. Below you can see an example object of that list.

{
   "player":"aicu",
   "uid":"C1-2-M57QKW5M68",
   "card_detail_id":2,
   "xp":7560,
   "gold":false,
   "edition":1,
   "market_id":"9c0434883e015f2d452aebf943dd8224ac22bc7d-1",
   "buy_price":"29.850",
   "market_listing_type":"RENT",
   "market_listing_status":0,
   "last_used_block":43230660,
   "last_used_player":"aicu",
   "last_used_date":"2020-05-09T17:22:03.000Z",
   "last_transferred_block":"None",
   "last_transferred_date":"None",
   "alpha_xp":0,
   "delegated_to":"None",
   "delegation_tx":"None",
   "skin":"None",
   "delegated_to_display_name":"None",
   "display_name":"None",
   "lock_days":"None",
   "unlock_date":"None",
   "level":10
}

1.2 Getting more details


Looks like we have all the information we need. But just in case lets implement another endpoint which gets even more card details:


def get_card_details():
    url: str = API2 + "/cards/get_details"
    return requests.get(url).json()

So we called just another endpoint, the endpoint returned another list of json objects, below an abbreviated version, but it already shows all the information we need.

{
   "id":1,
   "name":"Goblin Shaman",
   "color":"Red",
   "type":"Monster",
   "sub_type":null,
   "rarity":1,
   "drop_rate":80,
   .
   .
   .
}

Before we move on lets do some preprocessing of the data.

collection: Dict[str, dict] = {}
card_details: Dict[int, dict] = {}

collection_resp = get_card_collection("player_name")
details_resp = get_card_details()

for card in collection_resp:
    collection[card["uid"]] = card

for detail in details_resp:
    card_details[detail["id"]] = detail

We did that to make it easier and faster to look up data
later on. In programming speed is often in tradeoff with storage. In this case we increased the storage size by storing the data in a dictionary but also made the look up of the data faster compared to searching through a list (O(1) vs O(n)).

Last but not least lets declare our list of cards which we want to rent out:

cards_for_rental = {'C1-2-M57QKW5M68', 'C4-209-F0UTJ8IC5S'}

2. Getting Market Data


Now we have a lot of information about our card collection but still no market prices. Let's fix that:

def get_rental_listings(card_id: int, edition: int, gold: bool = False):
    url: str = API2 + "/market/for_rent_by_card"
    request: dict = {"card_detail_id": card_id,
                     "gold": str(gold).lower(),
                     "edition": edition}
    return requests.get(url, params=request).json()

This function calls the api and returns a list of all available rental offers for this card. Now let's connect our uid with this endpoint:

def get_rentals_by_uid(uid: str):
    uid_card = collection.get(uid)
    return get_rental_listings(uid_card["card_detail_id"],
                               uid_card["edition"],
                               uid_card["gold"])

We now have a function which takes our uid and returns all the rental prices currently on the market for that type of card. You might have noticed that our collection dictionary comes in handy now.

The endpoint returns a list of the following objects. We can see all details about the market listing, like type, seller and cost of the rental.

{
   "fee_percent":500,
   "uid":"C1-2-S91HZ9Q7KW",
   "seller":"doloknight",
   "card_detail_id":2,
   "xp":0,
   "gold":false,
   "edition":1,
   "buy_price":"0.195",
   "currency":"DEC",
   "desc":"None",
   "type":"RENT",
   "market_id":"b5bd9f79f39a289ed6d02c27286623176089c479-36",
   "last_transferred_block":"None",
   "last_transferred_date":"None",
   "last_used_block":58726972,
   "last_used_date":"2021-10-30T03:25:17.361Z",
   "last_used_player":"abdabiiz"
}

With that list we can figure out what the current market rate is for each card. But how do we do that ? Just use the lowest price in the list ? Lets do just that:

rental_list = get_rentals_by_uid(cards_for_rental[0])
rental_list.sort(key=lambda x: float(x["buy_price"]), reverse=False)
print(rental_list[0])
{
   "fee_percent":500,
   "uid":"C1-2-AW0GE0V0WW",
   "seller":"louis88",
   "card_detail_id":2,
   "xp":0,
   "gold":false,
   "edition":1,
   "buy_price":"0.100",
   "currency":"DEC",
   "desc":"None",
   "type":"RENT",
   "market_id":"dda6a28ad29965d7408af5a2b676d3a8ccfeea91-56",
   "last_transferred_block":"None",
   "last_transferred_date":"None",
   "last_used_block":60404653,
   "last_used_date":"2021-12-27T13:32:24.397Z",
   "last_used_player":"sp3ktraline"
}

That is the cheapest card on the market. If we offer our card at that price or below we're golden. Probably not. The thing is, this card is a level 1 card without any other cards merged into it (xp = 0). But the card I'm using is a level 10 card. We might get lucky and find a couple lvl 10 cards in there, but there's a better way:

Calculating the price per BCS which means calculating the price per single card. If we look at our two cards that would be:

11 XP for the lvl 4 chain golem and 7560 XP for the giant roc. What happened here ? Why does the legendary chain golem only have 11 XP and the giant roc over 7000 ?

That's because from untamed onwards XP is exactly the number of cards which were merged into it. Before that each merged card counted a different amount of XP. Which is also different for alpha, gold, and beta cards. And there's also the edge case of merging alpha cards into beta cards. Then the cards has some alpha xp as well. All in all it's a nightmare to compute it for all.

But I'm going to show you a way to compute BCX for untamed, alpha, beta and gold cards. But I'm going to omit the edge case with alpha xp, keeps the tutorial cleaner. It's not hard to do, just a bit messy.

3. Calculating BCX

First we need the XP tables for alpha, beta and gold cards, splinterlands offers those in the settings endpoint:

def get_settings():
    url: str = API2 + "/settings"
    return requests.get(url).json()

settings = get_settings()


Now let's calculate the bcx. For that I need a couple helper functions:

def determine_base_xp(rarity: int, xp_table: List[int], alpha: bool, beta: bool,
                      gold: bool):
    return xp_table[rarity - 1] if alpha or beta or gold else 1


def determine_xp_table(rarity: int, alpha: bool, beta: bool, gold: bool):
    xp_table: List[int] = []

    if alpha:
        xp_table = settings["beta_gold_xp"] if gold else settings["alpha_xp"]
    elif beta:
        xp_table = settings["beta_gold_xp"] if gold else settings["beta_xp"]
    else:
        xp_table = settings["gold_xp"] if gold else settings["xp_levels"][rarity - 1]

    return xp_table


def is_beta(card_detail_id: int, edition: int):
    return edition in [1, 2] or (edition == 3 and card_detail_id <= 223)


def is_alpha(edition: int):
    return edition == 0

Thats a lot to take in. The two helper fuctions is_beta and is_alpha are pretty much self explanatory. They determine whether a card is an alpha or beta card and therefor uses the legacy xp system. You might have noticed that we include edition 3 cards (reward cards) with a card_detail_id less than 223. Those cards also use the legacy xp system.

determine_xp_table returns the correct xp table and determine_base_xp returns the value we need to compute the actual amount of cards.

One note here: Technically we don't need the modern xp table (xp_levels) because for modern cards xp = BCX therefor we just return 1 for modern cards because for those xp is the same as the amount of cards as the base rate. You'll see why in a moment.

Computing BCX for an actual card:

def calc_bcx(uid: str):
    card_item = collection[uid]
    detail_id = card_item["card_detail_id"]
    edition = card_item["edition"]
    gold = card_item["gold"]
    xp = card_item["xp"]

    rarity: int = card_details[detail_id]["rarity"]
    beta: bool = is_beta(detail_id, edition)
    alpha: bool = is_alpha(edition)

    xp_table = determine_xp_table(rarity, alpha, beta, gold)
    xp_base = determine_base_xp(rarity, xp_table, alpha, beta, gold)
    add_inital_card: int = 0 if xp_base == 1 or gold else 1
    return (xp / xp_base) + add_inital_card

Lets go over the function. First we load all the information we need from our collection by uid. That would be the card_detail_id, card edition, whether its a gold card and the xp. Then we load the rarity of the card from the card_details.

Afterwards we determine whether its an alpha or beta card. Then we determine the correct xp table and the xp base rate.
Then we divide the xp by the "base rate" we determined. For alpha and beta cards we need to add one because the initial card is not included in the xp value. Gold cards seems to have it included.

Any other card editions just get divided by one which doesn't change the value.

Lets see if the function works, we're expecting 505 bcx for the first and 11 for the second:

print(calc_bcx(cards_for_rental[0])
=> 505.0
print(calc_bcx(cards_for_rental[1]))
=> 11.0

Looks good, now we're almost at the interesting part.

4. Computing price per bcx for all rental positions

We can now compute the BCX for our cards. Now we just need to figure out the price per BCX. If you remember the response from earlier we have a uid and a seller name. So we could in theory get the sellers collection and compute it that way. But if you look closely we essentially have all the information we need.

So we can save us this call, let's rewrite the calc_bcx bcs function a bit:

def calc_bcx_uid(uid: str):
    card_item = collection[uid]
    detail_id = card_item["card_detail_id"]
    edition = card_item["edition"]
    gold = card_item["gold"]
    xp = card_item["xp"]
    return calc_bcx(detail_id, edition, gold, xp)


def calc_bcx(detail_id: int, edition: int, gold: bool, xp):
    rarity: int = card_details[detail_id]["rarity"]
    beta: bool = is_beta(detail_id, edition)
    alpha: bool = is_alpha(edition)

    xp_table = determine_xp_table(rarity, alpha, beta, gold)
    xp_base = determine_base_xp(rarity, xp_table, alpha, beta, gold)
    add_card: int = 0 if xp_base == 1 or gold else 1
    return (xp / xp_base) + add_card

Now we have a function that can compute the bcx from minimal input and we reused it in the initial function. That saved us redundant code and a couple of api calls.

Now, lets compute the price per bcx for each market listing, choose the cheapest and compute our cards value:

def calc_price_per_bcx(uid: str):
    result = get_rentals_by_uid(uid)
    price_per_bcx = []
    for entry in result:
        per_bcx = float(entry["buy_price"]) / calc_bcx(entry["card_detail_id"], 
                                                       entry["edition"], 
                                                       entry["gold"],
                                                       entry["xp"])
        price_per_bcx.append(per_bcx)

    price_per_bcx.sort(key=lambda x: x, reverse=False)

    return price_per_bcx

Now we computed the price per bcx for each position. Just by dividing the buying price by the bcx of the card.

print(calc_price_per_bcx(cards_for_rental[0])[0])

Since the list is already sorted in ascending order we can just take the first value:

0.05584158415841584

Lets compute the price of our card:

print(lowest_ppbcx * calc_bcx_uid(cards_for_rental[0]))
=> 28.2 # 0.05584158415841584 * 505

Which tells us that our card is worth 28.2 DEC per day. Lets see how close that is to the cheapest card on the market:

image.png

Looks like we're spot on. The cheapest card by DEC/BCX value is 28.2 DEC Per Day. Now we can either pick that price or go a bit lower. That's up to you.

5. Posting, updating and deleting rental listings


We have our price and we have our cards. Now we just have to tell the website or rather the blockchain:

from beem import Hive

hive = Hive(keys=["ACTIVE_KEY","POSTING_KEY"])

Depending on the type of transaction we make we need different blockchain authorities. For deleting and creating rental listings we just need to posting key. For updating the active key is required.

Let's take a look at the transactions needed for creating, updating and deleting:


# Create market listing:
[ [ "custom_json", { "id": "sm_market_list", "json": "{\"cards\":[[\"G1-58-MB64QJHNF4\",149.9],
[\"G1-69-EQGGYGWAAO\",172.45],
[\"G3-89-5I1ZMYRS1C\",148.9],
[\"G4-209-2J82ZH9YXC\",224.65],
[\"G3-213-HN9UCR2M5S\",174.9]],
\"type\":\"rent\",\"fee\":500}", "required_auths": [],
 "required_posting_auths": [ "aicu" ] } ] ]
 
 # update price
 [ "custom_json", { "id": "sm_update_price", "json": "{\"ids\":[\"b532ae648a69b8fcbbf792848b0dde16af97c729-4\"],
\"new_price\":\"149.244\"}", "required_auths": [ "aicu" ],
 "required_posting_auths": [] } ]
 
#cancel rental
 { "id": "sm_market_cancel_rental", 
 "json": "{\"items\":[\"0ce7b01f1c95ba5229a420c8e719bfb7ff1b2370-23\"],
 

As you can see the structure for update and rental are pretty straight forward. But creating a market listing is a bit ugly. We can either stitch the needed json together or we use the inherent dictionary of python classes which are very similar to json.

Sounds complicated but its rather easy once you wrap your head around it.

I've contemplated using this technique in this tutorial but I think it makes things a lot easier to use and at the same time teaches a interesting technique.

To keep things orderly I recommend that you create a new python file. And place the following classes inside:

from typing import List, Tuple, Dict


class MarketListing:
    cards: List[List[any]]
    type: str
    fee: int
    """
    @param order_type: type of the order rent, sell etc
    @params order_fee: fee of the order taken by the marketplace, integer e.g. 500 = 5% .
    @param orders: List of Tuples where the first argument is the uid and the second the price in DEC
    """

    def __init__(self, order_type: str, order_fee: int, orders: List[Tuple[str, float]]):
        if orders:
            self.cards = []
            for order in orders:
                self.cards.append([order[0], order[1]])

        self.type = order_type
        self.fee = order_fee


class MarketUpdatePrice:
    class UpdatePriceItem:
        ids: List[str]
        new_price: float

        def __init__(self, price: float, market_id: str = None):
            self.ids = []
            if market_id:
                self.ids.append(market_id)
            self.new_price = price

        def append_id(self, market_id: str):
            if market_id:
                self.ids.append(market_id)

    orders: Dict[float, UpdatePriceItem] = {}
    """
        @param orders: List of Tuples where the first argument is the market id and the second the updated sprice in DEC

    """

    def __init__(self, orders: List[Tuple[str, float]]):
        if orders:
            for order in orders:
                order_entry = self.orders.get(order[1], self.UpdatePriceItem(order[1]))
                order_entry.append_id(order[0])
                self.orders[order[1]] = order_entry


class CancelRental:
    items: List[str]

    def __init__(self, items: List[str]):
        self.items = items


We now have three classes representing the three market operations.

CancelRental is pretty self explanatory. Its just a list of strings. Market ids in this case.

MarketUpdatePrice has some more logic build in. From the looks of it you can update the price for multiple market ids at once. So what this class does is take a list of tuples of market id and price and groups them in an internal object. Those objects mirror the json structure for a update price operation. I'll show you how to use it shortly after.

MarketListing mirrors exactly the structure of the sm_market_list operation. Turning a list of tuples, order type and fee into the required json structure.

Lets see how we use the new classes in our hive operation calls:

def create_listing(order_type: str, order_fee: int, orders: List[Tuple[str, float]]):
    listing: MarketListing = MarketListing(order_type, order_fee, orders)
    data = listing.__dict__
    hive.custom_json("sm_market_list", json_data=data,
                     required_posting_auths=["your_user"])


def update_prices(orders: List[Tuple[str, float]]):
    update_price: MarketUpdatePrice = MarketUpdatePrice(orders)

    for order in update_price.orders.values():
        data = order.__dict__
        hive.custom_json("sm_update_price", json_data=data,
                         required_auths=["your_user"])


def cancel_rental(rentals: List[str]):
    rentals = CancelRental(rentals)
    data = rentals.__dict__
    hive.custom_json("sm_market_cancel_rental", json_data=data,
                     required_posting_auths=["your_user"])

As you can see a "simple" call of "__dict__" on the class turns it into a dictionary which is for our use case good enough.

NOTE: Be careful if you plan on using this technique with booleans. They are serialized in python style meaning as True and False. Javascript and the splinterlands backend expects booleans in lowercase.

Now we have all the parts we need. Lets determine new prices for the flying roc and chain golem and submit it:

prices_for_update = []
new_listings = []

for uid in cards_for_rental:

    card = collection[uid]
    lowest_ppbcx = calc_price_per_bcx(uid)[0]
    new_price = lowest_ppbcx * calc_bcx_uid(uid)

    market_id = card["market_id"]
    if market_id and not card["delegated_to"]:
        prices_for_update.append((market_id, max(0.1, new_price)))
        print("adding updated price for ", uid, market_id, str(new_price))

    if not market_id:
        new_listings.append((uid, max(0.1, new_price)))
        print("creating new listing for ", uid, str(new_price))

adding updated price for  C4-209-F0UTJ8IC5S 5f2f2049bb2d5955fc32983c79039e3f77f144ec-24 72.85
[('5f2f2049bb2d5955fc32983c79039e3f77f144ec-24', 72.85)]
[]

In this loop we check all our cards and add all that are either listed and not rented or not listed at all to separate lists.

And looks like while I was typing this tutorial someone rented my giant roc. So no need to update to a lower price.

But the chain golem is still not delegated. Let's update the price from the old price of 73.300 to 72.85.

if prices_for_update:
    update_prices(prices_for_update)
if new_listings:
    create_listing('rent', 500, new_listings)

WARNING: don't use a upper case rent here. Splinterlands will interpret that as a normal sell operation.

NOTE the second parameter of 500 is the fee in percent. 500 equals a fee of 5%. If you want your rental to show up on the official splinterlands you need to use 5%. You can check the current "official" fee in the settings endpoint which we loaded earlier. You can look it up with the key "market_fee".

And voila, it worked. The corresponding hive transaction can be found here:
https://www.hiveblockexplorer.com/tx/83ac734f8fb04973215aa0c5bcd492a84bd61992

image.png

Now you have all the tools to load your card informations, get current rental prices, compute the price per bcx and update the price on splinterlands. If you're interested how to do scheduling in python and make it check every couple hours take a look at the library APScheduler.

Bonus - Posting only authority solution


And as a bonus:

prices_for_update = []
orders_for_deletion = []
new_listings = []

for uid in cards_for_rental:

    card = collection[uid]
    lowest_ppbcx = calc_price_per_bcx(uid)[0]
    new_price = lowest_ppbcx * calc_bcx_uid(uid)
    print(card)
    market_id = card["market_id"]
    if market_id and not card["delegated_to"]:
        orders_for_deletion.append(market_id)
        prices_for_update.append((uid, max(0.1, new_price)))
        print("creating new listing for ", uid, market_id, str(new_price))

    if not market_id:
        new_listings.append((uid, max(0.1, new_price)))
        print("creating new listing for ", uid, str(new_price))

print(prices_for_update)
print(new_listings)
print(orders_for_deletion)

if prices_for_update:
    cancel_rental(orders_for_deletion)
    time.sleep(5)
    create_listing('rent', 500, prices_for_update)
if new_listings:
    create_listing('rent', 500, new_listings)

This version doesn't require a active key.

For everyone who just wants the whole solution:
File: MarketHiveTransactions.py

from typing import List, Tuple, Dict


class MarketListing:
    cards: List[List[any]]
    type: str
    fee: int
    """
    @param order_type: type of the order rent, sell etc
    @params order_fee: fee of the order taken by the marketplace, integer e.g. 500 = 5% .
    @param orders: List of Tuples where the first argument is the uid and the second the price in DEC
    """

    def __init__(self, order_type: str, order_fee: int, orders: List[Tuple[str, float]]):
        if orders:
            self.cards = []
            for order in orders:
                self.cards.append([order[0], order[1]])

        self.type = order_type
        self.fee = order_fee


class MarketUpdatePrice:
    class UpdatePriceItem:
        ids: List[str]
        new_price: float

        def __init__(self, price: float, market_id: str = None):
            self.ids = []
            if market_id:
                self.ids.append(market_id)
            self.new_price = price

        def append_id(self, market_id: str):
            if market_id:
                self.ids.append(market_id)

    orders: Dict[float, UpdatePriceItem] = {}
    """
        @param orders: List of Tuples where the first argument is the market id and the second the updated sprice in DEC

    """

    def __init__(self, orders: List[Tuple[str, float]]):
        if orders:
            for order in orders:
                order_entry = self.orders.get(order[1], self.UpdatePriceItem(order[1]))
                order_entry.append_id(order[0])
                self.orders[order[1]] = order_entry


class CancelRental:
    items: List[str]

    def __init__(self, items: List[str]):
        self.items = items

File: main.py

import time
from typing import Dict, List, Tuple

import requests
from beem import Hive

from MarketHiveTransactions import MarketUpdatePrice, MarketListing, CancelRental

API2 = "https://api2.splinterlands.com"


def get_card_collection(player):
    url: str = API2 + "/cards/collection/" + player
    return requests.get(url).json()["cards"]


def get_card_details():
    url: str = API2 + "/cards/get_details"
    return requests.get(url).json()


collection: Dict[str, dict] = {}
card_details: Dict[int, dict] = {}

collection_resp = get_card_collection("aicu")
details_resp = get_card_details()

for card in collection_resp:
    collection[card["uid"]] = card

for detail in details_resp:
    card_details[detail["id"]] = detail


def get_rental_listings(card_id: int, edition: int, gold: bool = False):
    url: str = API2 + "/market/for_rent_by_card"
    request: dict = {"card_detail_id": card_id,
                     "gold": str(gold).lower(),
                     "edition": edition}
    return requests.get(url, params=request).json()


def get_rentals_by_uid(uid: str):
    uid_card = collection.get(uid)
    return get_rental_listings(uid_card["card_detail_id"],
                               uid_card["edition"],
                               uid_card["gold"])


def get_settings():
    url: str = API2 + "/settings"
    return requests.get(url).json()


settings = get_settings()


def determine_base_xp(rarity: int, xp_table: List[int], alpha: bool, beta: bool,
                      gold: bool):
    return xp_table[rarity - 1] if alpha or beta or gold else 1


def determine_xp_table(rarity: int, alpha: bool, beta: bool, gold: bool):
    xp_table: List[int] = []

    if alpha:
        xp_table = settings["beta_gold_xp"] if gold else settings["alpha_xp"]
    elif beta:
        xp_table = settings["beta_gold_xp"] if gold else settings["beta_xp"]
    else:
        xp_table = settings["gold_xp"] if gold else settings["xp_levels"][rarity - 1]

    return xp_table


def is_beta(card_detail_id: int, edition: int):
    return edition in [1, 2] or (edition == 3 and card_detail_id <= 223)


def is_alpha(edition: int):
    return edition == 0


def calc_bcx_uid(uid: str):
    card_item = collection[uid]
    detail_id = card_item["card_detail_id"]
    edition = card_item["edition"]
    gold = card_item["gold"]
    xp = card_item["xp"]
    return calc_bcx(detail_id, edition, gold, xp)


def calc_bcx(detail_id: int, edition: int, gold: bool, xp):
    rarity: int = card_details[detail_id]["rarity"]
    beta: bool = is_beta(detail_id, edition)
    alpha: bool = is_alpha(edition)

    xp_table = determine_xp_table(rarity, alpha, beta, gold)
    xp_base = determine_base_xp(rarity, xp_table, alpha, beta, gold)
    add_card: int = 0 if xp_base == 1 or gold else 1
    return (xp / xp_base) + add_card  # +1 because the initial card doesn't get counted in xp


def calc_price_per_bcx(uid: str):
    result = get_rentals_by_uid(uid)
    price_per_bcx = []
    for entry in result:
        per_bcx = float(entry["buy_price"]) / calc_bcx(entry["card_detail_id"],
                                                       entry["edition"],
                                                       entry["gold"],
                                                       entry["xp"])
        price_per_bcx.append(per_bcx)

    price_per_bcx.sort(key=lambda x: x, reverse=False)

    return price_per_bcx


hive = Hive(keys=["PRIVATE_POSTING", "PRIVAT_ACTIVE"])


def create_listing(order_type: str, order_fee: int, orders: List[Tuple[str, float]]):
    listing: MarketListing = MarketListing(order_type, order_fee, orders)
    data = listing.__dict__
    hive.custom_json("sm_market_list", json_data=data,
                     required_posting_auths=["your_username"])


def update_prices(orders: List[Tuple[str, float]]):
    update_price: MarketUpdatePrice = MarketUpdatePrice(orders)

    for order in update_price.orders.values():
        data = order.__dict__
        hive.custom_json("sm_update_price", json_data=data,
                         required_auths=["your_username"])


def cancel_rental(rentals: List[str]):
    rentals = CancelRental(rentals)
    data = rentals.__dict__
    hive.custom_json("sm_market_cancel_rental", json_data=data,
                     required_posting_auths=["your_username"])


prices_for_update = []
new_listings = []

cards_for_rental = ['C1-2-M57QKW5M68', 'C4-209-F0UTJ8IC5S']

for uid in cards_for_rental:

    card = collection[uid]
    lowest_ppbcx = calc_price_per_bcx(uid)[0]
    new_price = lowest_ppbcx * calc_bcx_uid(uid)
    print(card)
    market_id = card["market_id"]
    if market_id and not card["delegated_to"]:
        prices_for_update.append((market_id, max(0.1, new_price)))
        print("adding updated price for ", uid, market_id, str(new_price))

    if not market_id:
        new_listings.append((uid, max(0.1,new_price)))
        print("creating new listing for ", uid, str(new_price))

print(prices_for_update)
print(new_listings)

if prices_for_update:
    update_prices(prices_for_update)
if new_listings:
    create_listing('rent', 500, new_listings)

That's it. Thanks for reading all the way until here. The tutorial is quite the wall of text. And thank you to @epicraptor (SplinterForge.io) and @furryboffin from the discord ( I'm not sure if i got those users right).

And like always a small giveaway. Since we've talked so much about chain golems I'm going to giveaway two chain golem lvl 1. Just comment your splinterlands account below. Winners will be drawn at random after 7 days:

Chain golem lvl 1 C4-209-60U68UYTTC
Chain golem lvl 1 C4-209-SGNHZJWN00

Sort:  

Wow, this is amazing. I had no idea that you could do all of that with python. Thank you so much for the tutorial. This is really great :-D

hi, glad you like it. and yes, you can do pretty much anything with python ;)

I'm still too new to Python to tackle this right now but I'm saving this to attempt it later. Great work!

!PIZZA

it's actually not that difficult. just get started and you'll notice it sooner rather than later, learning by doing :)

Maybe the new Ragnarok is something for me. I skipped Splinterlands and quit Gods Unchained which was fun though but I loose to kids who just buya lot of powerfull cards.

hi @goldrooster ,

the new ragnarok ? I'm confused, are you talking about Ragnarok M: Eternal Love or the record of ragnarok ?

https://peakd.com/hive-131619/@ragnarok.game/ragnarok-is-upon-us

A new card game for hivers only in 1st instance. With an airdrop of cards. More has been written recently.

thanks for the tip. that looks interesting. so the airdrop you get is based on how much hive/hp you hold at the snapshot day ?

That is what I read yes.

Wow that is very cool!
@snaqz

Hey @snaqz , thanks :)

Übrigens, aus Neugierde: hab gesehen, dass du einen Rental Service anbietest. Ist der automatisiert, oder machst du den von Hand ?

Den mache ich per Hand :) Hab dann durch Zufall deinen Post gesehen, wie das manchmal so ist :) Mal schauen ob ich das vielleicht sogar noch besser automatisiert hinbekommen würde. Finde aber manchmal gibt es Sachen die kann ein Bot nicht entscheiden :)

Cool, wie lange machst du den schon ?

und: was meinst du, was sind das für Entscheidungen welche einem Bot schwer fallen?

Seit kurzem erst :)

Naja es werden häufig Karten viel zu hoch angeboten, dass dort keine Vermietung zustande kommt, gerade am Anfang der Season. Das zieht sich häufig bei einigen Karten auch gerne die ganze Season über und diese werden erst gegen Ende vermietet. Dort trifft die Nachfrage einfach noch nicht auf das Angebot. Natürlich sind die TOP Karten immer schnell und teuer vermietet, aber beim maximieren der Rendite geht es darum im ständigen Wechsel alle Karten bestmöglich zu vermieten.

Müsste ich mir mal Gedanken machen wie man das aus der API auslesen könnte. Möglich wäre das bestimmt. Bin da nun aber kein Experte auf dem Gebiet :)

Wie wäre folgendes Vorgehen:

  1. vermiete karten immer zum besten (niedrigsten per bcx) preis.

  2. lasse die karte vermietet bis der aktuelle niedrigste marktpreis x mal soviel ist wie der aktuelle preis deiner Vermietung. z.b. 1.5, 2 oder 3 mal. dann brich das rental ab und stell sie zu dem neuen preis auf den markt.

Damit solltest du sicherstellen, dass a) die karten immer vermietet sind (weil am günstigens per bcx) und du keine verluste machst weil der "rental" marketwert angestiegen ist.

Klingt sehr gut :)
Aber die Karten werden zum niedrigsten per bcx Preis oft nicht vermietet. Da muss man irgendwie ein Gefühl für bekommen :)

PIZZA!

PIZZA Holders sent $PIZZA tips in this post's comments:
@cryptoace33(1/5) tipped @bauloewe (x1)
tfranzini tipped bauloewe (x1)

You can now send $PIZZA tips in Discord via tip.cc!

Thanks for the great tutorial!

@cs50x

First of all, I would like to appreciate all your time, attention and detail put on this thread. I have been delaying learning python and you just gave me a great will to finally start.

However, I have been getting erros at the function get_rental_listing. It doesn't matter what I do, it always returns "None". I've tried my own cards that were renting already, but also cards there were not renting and I always get the same result.

I believe the error is not on the function itself, but the api key:

"https://api2.splinterlands.com//market/for_rent_by_card"

Is this api key correct? I tried looking up at kiokizz github api documentation, but there isn't any related to rent, only sale. (https://github.com/kiokizz/Splinterlands-API)

Once again, thank you very much for what you are doing for the community. As a busy man who can't be looking at my rentals all day long, this is awesome.

And again, Thanks for pointing the mistakes out. take the upvote of me and my humble alt account ;)

if your cards have any value paste pure CP theis discounts them up to 80% I have spent literally a day trying to fix it. Almost evil have useful and helpful guide i only to lead to a bot that would take a 1000/DEC day rentals and turn it into 150. I dont care about DEC but I wish I never saw this. Why would you go to such lengths to price by BCX? Thats crazy. It ruined me.

thanks for the tip.

and yes I'm aware that the cheapest per bcx price is not the best price for a card. But the tutorial teaches all you need to know about renting out cards and take market prices into account.

With the building blocks you learned in this tutorial you can build a custom pricing strategy for optimal returns.

that was written out frustration from my lack of coding, and also I had some cards that had no hits on the scan get listed for 0.1 DEC as well. I should have been grateful in the post it was an expression of the madness of trying to code python without a proper base in it, then a critique of your guide despite how it came off. cheers.

Loading...

Alright, with some delay it's giveaway time, and the winners are:

@cs50x
@snaqz

Cards should arrive in your accounts shorty.

Wow, thank you so much again!

I've been writing Python every day lately, and I've created an auto-renewal script for rentals and a tool similar to Peakmonsters' BID.

I'm looking forward to learning more from your articles.

Wow! Thank you so much! I really appreciate it!🤝👍

@chris.topher @tfranzini @snaqz @cs50x
A warning: make sure, that the market type rent is in lowercase. Seems like "RENT" triggers a normal market sell. Fixed it in the tutorial above. But just wanted to make sure that nobody made the same mistake as me^^

Also added a version which doesn't require a active key ( first delete, then create new listing)

Thanks for the heads up!

thank you for the warning :)

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

You received more than 2750 upvotes.
Your next target is to reach 3000 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:

PUD - PUH - PUM - It's all about to Power Up!
Christmas Challenge - 1000 Hive Power Delegation Winner
Support the HiveBuzz project. Vote for our proposal!

i will try it today! ty for sharing IGN: tuzia

hi tuzia, thanks, hope it worked out for you (:

and sadly the giveaway has been closed long ago.

It's wonderful you took the time to write all of this up. Thanks
!PIZZA
!BEER