Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pricing-tf"
version = "4.0.0"
version = "4.0.1"
edition = "2024"

[dependencies]
Expand Down
12 changes: 11 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
FROM rust:1.92.0-alpine3.23 AS builder
FROM --platform=$BUILDPLATFORM rust:1.92.0-alpine3.23 AS builder
ARG TARGETPLATFORM

WORKDIR /app
# Busybox bug on arm64, remove the flags when alpine is fixed
RUN apk upgrade --no-cache --scripts=no apk-tools && apk add --no-cache protobuf-dev postgresql-dev
# Map $TARGETPLATFORM to $RUST_TARGET used by cargo
RUN case "${TARGETPLATFORM}" in \
"linux/amd64") export RUST_TARGET="x86_64-unknown-linux-musl";; \
"linux/arm64") export RUST_TARGET="aarch64-unknown-linux-musl";; \
*) echo "Unsupported TARGETPLATFORM ${TARGETPLATFORM}"; exit 1;; \
esac && \
rustup target add $RUST_TARGET
ENV CARGO_TARGET_DIR=/app/target

COPY Cargo.toml Cargo.lock build.rs ./
COPY ./protos/ ./protos/
Expand Down
113 changes: 113 additions & 0 deletions migrations/2025-12-14-190302-0000_trade_item_id/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
alter table trade_listings
drop column trade_item_details_id;

drop index uq_mv_prices_item_name;
drop index uq_mv_bot_prices_item_name;
drop materialized view mv_prices;
drop materialized view mv_bot_prices;

create materialized view mv_prices as
with
formatted_listings as (
select
item_name,
intent,
bumped_at,
price_keys,
jsonb_build_object(
'price', jsonb_build_object(
'keys', original_price_keys,
'metal', original_price_metal
),
'trade_details', jsonb_build_object(
'listing_id', id,
'trade_offer_url', trade_details_trade_offer_url,
'description', trade_details_description,
'item', jsonb_build_object(
'name', item_name,
'image_url', trade_item_details_image_url,
'quality', jsonb_build_object(
'id', item_quality_id,
'name', item_quality_name,
'color', item_quality_color
)
),
'user', jsonb_build_object(
'name', trade_user_details_name,
'avatar_thumbnail_url', trade_user_details_avatar_thumbnail_url,
'online', trade_user_details_online,
'steam_id', trade_user_details_steam_id
)
),
'bumped_at', bumped_at,
'price_keys', price_keys,
'price_metal', price_metal
) as listing_json
from trade_listings
)
select
item_name,
coalesce(jsonb_agg(listing_json order by price_keys DESC, bumped_at DESC)
filter (where intent = 'buy'), '[]'::jsonb) as buy_listings,
coalesce(jsonb_agg(listing_json order by price_keys ASC, bumped_at DESC)
filter (where intent = 'sell'), '[]'::jsonb) as sell_listings,
MAX(bumped_at) as updated_at
from formatted_listings
group by item_name;

-- Unique index for concurrent refreshes
create unique index uq_mv_prices_item_name on mv_prices (item_name);

create materialized view mv_bot_prices as
with
formatted_listings as (
select
item_name,
intent,
is_automatic,
bumped_at,
price_keys,
jsonb_build_object(
'price', jsonb_build_object(
'keys', original_price_keys,
'metal', original_price_metal
),
'trade_details', jsonb_build_object(
'listing_id', id,
'trade_offer_url', trade_details_trade_offer_url,
'description', trade_details_description,
'item', jsonb_build_object(
'name', item_name,
'image_url', trade_item_details_image_url,
'quality', jsonb_build_object(
'id', item_quality_id,
'name', item_quality_name,
'color', item_quality_color
)
),
'user', jsonb_build_object(
'name', trade_user_details_name,
'avatar_thumbnail_url', trade_user_details_avatar_thumbnail_url,
'online', trade_user_details_online,
'steam_id', trade_user_details_steam_id
)
),
'bumped_at', bumped_at,
'price_keys', price_keys,
'price_metal', price_metal
) as listing_json
from trade_listings
where is_automatic = true
)
select
item_name,
coalesce(jsonb_agg(listing_json order by price_keys DESC, bumped_at DESC)
filter (where intent = 'buy'), '[]'::jsonb) as buy_listings,
coalesce(jsonb_agg(listing_json order by price_keys ASC, bumped_at DESC)
filter (where intent = 'sell'), '[]'::jsonb) as sell_listings,
MAX(bumped_at) as updated_at
from formatted_listings
group by item_name;

-- Unique index for concurrent refreshes
create unique index uq_mv_bot_prices_item_name on mv_bot_prices (item_name);
115 changes: 115 additions & 0 deletions migrations/2025-12-14-190302-0000_trade_item_id/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
alter table trade_listings
add column trade_item_details_id text default '' not null;

drop index uq_mv_prices_item_name;
drop index uq_mv_bot_prices_item_name;
drop materialized view mv_prices;
drop materialized view mv_bot_prices;

create materialized view mv_prices as
with
formatted_listings as (
select
item_name,
intent,
bumped_at,
price_keys,
jsonb_build_object(
'price', jsonb_build_object(
'keys', original_price_keys,
'metal', original_price_metal
),
'trade_details', jsonb_build_object(
'listing_id', id,
'trade_offer_url', trade_details_trade_offer_url,
'description', trade_details_description,
'item', jsonb_build_object(
'id', trade_item_details_id,
'name', item_name,
'image_url', trade_item_details_image_url,
'quality', jsonb_build_object(
'id', item_quality_id,
'name', item_quality_name,
'color', item_quality_color
)
),
'user', jsonb_build_object(
'name', trade_user_details_name,
'avatar_thumbnail_url', trade_user_details_avatar_thumbnail_url,
'online', trade_user_details_online,
'steam_id', trade_user_details_steam_id
)
),
'bumped_at', bumped_at,
'price_keys', price_keys,
'price_metal', price_metal
) as listing_json
from trade_listings
)
select
item_name,
coalesce(jsonb_agg(listing_json order by price_keys DESC, bumped_at DESC)
filter (where intent = 'buy'), '[]'::jsonb) as buy_listings,
coalesce(jsonb_agg(listing_json order by price_keys ASC, bumped_at DESC)
filter (where intent = 'sell'), '[]'::jsonb) as sell_listings,
MAX(bumped_at) as updated_at
from formatted_listings
group by item_name;

-- Unique index for concurrent refreshes
create unique index uq_mv_prices_item_name on mv_prices (item_name);

create materialized view mv_bot_prices as
with
formatted_listings as (
select
item_name,
intent,
is_automatic,
bumped_at,
price_keys,
jsonb_build_object(
'price', jsonb_build_object(
'keys', original_price_keys,
'metal', original_price_metal
),
'trade_details', jsonb_build_object(
'listing_id', id,
'trade_offer_url', trade_details_trade_offer_url,
'description', trade_details_description,
'item', jsonb_build_object(
'id', trade_item_details_id,
'name', item_name,
'image_url', trade_item_details_image_url,
'quality', jsonb_build_object(
'id', item_quality_id,
'name', item_quality_name,
'color', item_quality_color
)
),
'user', jsonb_build_object(
'name', trade_user_details_name,
'avatar_thumbnail_url', trade_user_details_avatar_thumbnail_url,
'online', trade_user_details_online,
'steam_id', trade_user_details_steam_id
)
),
'bumped_at', bumped_at,
'price_keys', price_keys,
'price_metal', price_metal
) as listing_json
from trade_listings
where is_automatic = true
)
select
item_name,
coalesce(jsonb_agg(listing_json order by price_keys DESC, bumped_at DESC)
filter (where intent = 'buy'), '[]'::jsonb) as buy_listings,
coalesce(jsonb_agg(listing_json order by price_keys ASC, bumped_at DESC)
filter (where intent = 'sell'), '[]'::jsonb) as sell_listings,
MAX(bumped_at) as updated_at
from formatted_listings
group by item_name;

-- Unique index for concurrent refreshes
create unique index uq_mv_bot_prices_item_name on mv_bot_prices (item_name);
2 changes: 2 additions & 0 deletions protos/pricing_tf/pricing/v1/pricing_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ message ItemQuality {
}

message TradeItemDetails {
// item id from Steam. Empty string for buy listings
string id = 4;
string name = 1;
string image_url = 2;
ItemQuality quality = 3;
Expand Down
38 changes: 21 additions & 17 deletions src/backpack_tf_api/facade.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use crate::backpack_tf_api::payloads::{BackpackTfApiError, SnapshotResponse};
use crate::models::trade_listing::ListingIntent;

Expand Down Expand Up @@ -33,28 +35,30 @@ impl BackpackTfApi {
.json()
.await?;

let mut sell_listings_iter = response
let first_price = response
.listings
.iter()
.filter(|listing| listing.intent == ListingIntent::Sell);

let mut price_mode = sell_listings_iter
.next()
.first()
.ok_or(BackpackTfApiError::NoSellListings)?
.price;
let mut current_diff = 1;

for listing in sell_listings_iter {
if listing.price == price_mode {
current_diff += 1;
} else if current_diff == 0 {
price_mode = listing.price;
current_diff = 1;
} else {
current_diff -= 1;

let sell_listings = response
.listings
.into_iter()
.filter(|listing| listing.intent == ListingIntent::Sell);

let mut counter = HashMap::<u16, u8>::new();
for listing in sell_listings {
// convert to metal "cents" to avoid floating point precision issues
// e.g. 65.33 metal -> 6533
let price = (listing.price * 100.0).round() as u16;
let entry = counter.entry(price).or_insert(0);
*entry += 1;
if *entry >= 3 {
return Ok(listing.price);
}
}

Ok(price_mode)
// fallback simply the lowest sell price
Ok(first_price)
}
}
5 changes: 3 additions & 2 deletions src/backpack_tf_api/payloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ impl From<reqwest::Error> for BackpackTfApiError {
}
}

#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
pub struct SnapshotResponse {
/// Listings come sorted by price, first all lowest sell listings, then highest buy listings
pub listings: Vec<SnapshotListing>,
}

/// More fields are present in the response, add if needed
#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
pub struct SnapshotListing {
pub steamid: String,
/// full metal conversion
Expand Down
6 changes: 6 additions & 0 deletions src/models/trade_listing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct ItemQuality {

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TradeItemDetails {
pub id: String,
pub image_url: String,
pub quality: ItemQuality,
}
Expand Down Expand Up @@ -76,6 +77,7 @@ pub struct TradeListingRow {
pub trade_details_trade_offer_url: String,
pub trade_details_description: String,

pub trade_item_details_id: String,
pub trade_item_details_image_url: String,

pub item_quality_id: i32,
Expand All @@ -98,6 +100,7 @@ impl From<TradeListingRow> for TradeListing {
};

let trade_item_details = TradeItemDetails {
id: row.trade_item_details_id,
image_url: row.trade_item_details_image_url,
quality: item_quality,
};
Expand Down Expand Up @@ -133,6 +136,7 @@ impl From<TradeListingRow> for TradeListing {

#[derive(Insertable)]
#[diesel(table_name = crate::schema::trade_listings)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct NewTradeListing {
pub id: String,
pub item_name: String,
Expand All @@ -148,6 +152,7 @@ pub struct NewTradeListing {
pub trade_details_trade_offer_url: String,
pub trade_details_description: String,

pub trade_item_details_id: String,
pub trade_item_details_image_url: String,

pub item_quality_id: i32,
Expand Down Expand Up @@ -177,6 +182,7 @@ impl From<TradeListing> for NewTradeListing {

trade_details_trade_offer_url: model.trade_details.trade_offer_url,
trade_details_description: model.trade_details.description,
trade_item_details_id: model.trade_details.item.id,
trade_item_details_image_url: model.trade_details.item.image_url,

item_quality_id: model.trade_details.item.quality.id as i32,
Expand Down
Loading