Programming Tutorial: Renting your splinterlands card collection with python

avatar
(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



0
0
0.000
38 comments
avatar

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

0
0
0.000
avatar

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

0
0
0.000
avatar

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!
0
0
0.000
avatar

Wow that is very cool!
@snaqz

0
0
0.000
avatar
(Edited)

Hey @snaqz , thanks :)

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

0
0
0.000
avatar

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 :)

0
0
0.000
avatar
(Edited)

Cool, wie lange machst du den schon ?

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

0
0
0.000
avatar

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 :)

0
0
0.000
avatar
(Edited)

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.

0
0
0.000
avatar

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 :)

0
0
0.000
avatar
(Edited)

Warum würde die günstigste Karte auf dem Markt nicht vermietet werden ? Das geht doch komplett gegen Angebot und Nachfrage.

Einziger Fall den ich mir vorstellen kann ist, dass wenn eine Karte z.b. mehr bcx hat als eine andere, somit teurer ist pro bcx aber noch nicht das nächste level erreicht hat somit nicht mehr Funktionalität bietet.

Jedenfalls, vermiete mein Deck seit kurzem semi automatisch und bin bisher ok gefahren ca 10k DEC pro Tag.

Habs jetzt seit gestern voll Automatisiert und werde mal beobachten wie das ganze läuft.

0
0
0.000
avatar

Cool! Weil der niedrigste Preis bei einigen Karten x2 x3 x4 so hoch ist als das Sie dann vermietet werden. Gerade zum Season Ende fällt mir wieder auf das einige Karten für 30 CP/DEC drin sind welche dann natürlich selten vermietet werden. Ich denke da an Corrupted Pegasus, Lord of Darkness, Defender of Truth usw :)
Die werden gerne mal für 100 DEC auf Lvl 2 oder 3 angeboten wobei man die für 30-60 DEC super vermieten kann. (Zahlen nur als Beispiel)

Ich werde mich wenn ich die Zeit finde da auch mal ransetzen und versuchen etwas zu automatisieren, aber dennoch werde ich drüber schauen :)

0
0
0.000
avatar

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

!PIZZA

0
0
0.000
avatar

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

0
0
0.000
avatar
(Edited)

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!

0
0
0.000
avatar
(Edited)

@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)

0
0
0.000
avatar

thank you for the warning :)

0
0
0.000
avatar

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.

0
0
0.000
avatar

hi @goldrooster ,

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

0
0
0.000
avatar

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

@cs50x
@snaqz

Cards should arrive in your accounts shorty.

0
0
0.000
avatar

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.

0
0
0.000
avatar

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

0
0
0.000
avatar

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.

0
0
0.000
avatar

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

0
0
0.000
avatar

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

0
0
0.000
avatar

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

and sadly the giveaway has been closed long ago.

0
0
0.000
avatar

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.

0
0
0.000
avatar

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.

0
0
0.000
avatar

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.

0
0
0.000
avatar

Much appreciated for this tutorial on the rental system. I have a basic understanding of the API since I've used it to collect data and stats for Excel, but wanted to go for a Python approach with it.

I've made a few modifications to the code that I think some would find useful.

First of, the API caller URL is at the moment of writing https://cache-api.splinterlands.com/.

Rewriting Market Update Prices

The rental system has updated the API call for updating the market prices for rentals.

Changes here are

  • Rewriting the MarketUpdatePrice class in the MarketHiveTransactions.py file
  • Changing custom_json to sm_update_rental_price
  • Change how we append to the update_prices, now use the market_id instead.
def update_prices(orders: List[Tuple[str, float]], auth_user:str):
    items: MarketUpdatePrice = MarketUpdatePrice(orders)

    data = items.__dict__
    hive.custom_json("sm_update_rental_price", json_data=data,
                    required_auths=[auth_user])
class MarketUpdatePrice:
    items: List[List[any]]

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


prices_for_update.append((card["market_id"], max(0.1, new_price)))

Get the best BCX price for your cards XP

I changed the way the calc_price_per_bcx function selects the best entry.
The issue, as some has noted here in comments, is that some low cards can have a terrible price match compared to higher cards as they are usually dumped on the market.

Instead, here we make sure we only use cards with equal or higher XP to match with. We also filter out any of our own entries, as we want to know the best other price to match with. If we would not do this, and our own price is the best, we would match against that one.

Changes here are

  • Get the card data from collection Dict.
  • Match only with equal or higher XP
  • Match only if we are not the seller
def calc_price_per_bcx(uid: str, collection, card_details, settings, auth_user:str):
    result = get_rentals_by_uid(uid, collection)
    card = collection[uid]
    price_per_bcx = []
    for entry in result:
        if entry["xp"] >= card["xp"] and entry["seller"] != auth_user:
            per_bcx = float(entry["buy_price"]) / calc_bcx(entry["card_detail_id"], 
                                                       entry["edition"], 
                                                       entry["gold"],
                                                       entry["xp"],
                                                       card_details,
                                                       settings)
            price_per_bcx.append(per_bcx)

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

    return price_per_bcx[0]

I have also made a few other modifications as I created a seperate file for Splinterlands API functions. This is why a few of the function classes have more items than the original code.

0
0
0.000
avatar
(Edited)

Hey @sc-steemit,

judging by your username you've been around for some time ;). Thanks for pointing the changes out.

  1. When did they change the base API url ? Didn't catch that change at all.

  2. Whats up with sm_update_rental_price ? Is it a new operation type ? Any advantage ?

Edit: filtering the prices by level is a good idea. although you could also look into the for_rent_grouped endpoint. saves a LOT of api calls. It's a bit delayed, but i haven't noticed a large difference in rental revenue between the two.

0
0
0.000
avatar

True, been around for a while, when steemit was the main platform :)
I like automation and Python, and loved your detailed info on how you made it work, helped me out a lot.

  1. I can't recall why exactly, but I guess it's all about optimization. It's the API url that peakmonsters use, and has been working well since a few months ago. Can't find it in the official changelog though.

  2. sm_update_rental_price is documented here. Not sure when it was implemented, but I guess it was with the new rental system came to place? The advantage is that you don't have to send more than one transaction with all the updates, instead of cancelling and publishing. Also, I don't think the transaction type used in the OP is working any more.

I considered making a call for the API for_rent_grouped, but as you can't call for a specific card ID you have to pull the entire list for all ten market levels. May save some though, as per card data is pretty much as well. You just have to keep track on if your current listprice is the same so you don't underbid yourself.

0
0
0.000
avatar

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

0
0
0.000