Thanks to visit codestin.com
Credit goes to lib.rs

#trading #warframe #market #game #api

wf-market

A Rust client library for the warframe.market API

15 releases

Uses new Rust 2024

new 0.3.3 Jan 11, 2026
0.3.2 Jan 11, 2026
0.2.2 Jan 10, 2026
0.1.11 Jun 13, 2025

#251 in Game dev

GPL-3.0-only

340KB
6K SLoC

wf-market

A Rust client library for the warframe.market API.

Crates.io Documentation License: GPL-3.0

Features

  • Type-safe API - Compile-time guarantees prevent common mistakes like updating orders you don't own
  • Automatic item loading - Items are fetched on client construction; orders have direct item access via get_item()
  • Async/await - Built on Tokio for efficient async operations
  • Session persistence - Save and restore login sessions with serde-compatible credentials
  • Rate limiting - Built-in rate limiter to prevent API throttling
  • Caching - Optional caching for slowly-changing data (items, rivens)
  • WebSocket support - Real-time order updates (optional feature)

Installation

Add to your Cargo.toml:

[dependencies]
wf-market = "0.2"

# With WebSocket support
wf-market = { version = "0.2", features = ["websocket"] }

Quick Start

use wf_market::{Client, Credentials, CreateOrder};

#[tokio::main]
async fn main() -> wf_market::Result<()> {
    // Create an unauthenticated client (fetches items automatically)
    let client = Client::builder().build().await?;
    
    // Items are already loaded - access them directly
    println!("Loaded {} items", client.items().len());
    
    // Get orders for an item
    let orders = client.get_orders("nikana_prime_set").await?;
    for order in orders.iter().take(5) {
        // Orders have direct item access via get_item()
        if let Some(item) = order.get_item() {
            println!("{}: {}p for {}", order.user.ingame_name, order.order.platinum, item.name());
        }
    }
    
    // Login for authenticated operations
    let creds = Credentials::new(
        "[email protected]",
        "password",
        Credentials::generate_device_id(),
    );
    let client = client.login(creds).await?;
    
    // Create an order
    let order = client.create_order(
        CreateOrder::sell("nikana_prime_set", 100, 1)
    ).await?;
    println!("Created order: {}", order.id());
    
    Ok(())
}

Authentication

Login with Credentials

use wf_market::{Client, Credentials};

let creds = Credentials::new("[email protected]", "password", Credentials::generate_device_id());
let client = Client::builder().build().await?.login(creds).await?;

Session Persistence

Save and restore sessions to avoid re-authenticating:

use wf_market::{Client, Credentials};

// After login, export the session
let session = client.export_session();
let json = serde_json::to_string(&session)?;
std::fs::write("session.json", &json)?;

// Later: restore session
let saved: Credentials = serde_json::from_str(&std::fs::read_to_string("session.json")?)?;

// Validate before using (recommended)
if Client::validate_credentials(&saved).await? {
    let client = Client::builder().build().await?.login(saved).await?;
}

Working with Orders

Fetching Orders

// Get orders with user info
let orders = client.get_orders("nikana_prime_set").await?;

// Orders have direct item access
for order in &orders {
    if let Some(item) = order.get_item() {
        println!("{} - {}p by {}", item.name(), order.order.platinum, order.user.ingame_name);
    }
}

// Get just order data (lighter response)
let listings = client.get_listings("nikana_prime_set").await?;

// Get top buy/sell prices
let top = client.get_top_orders("nikana_prime_set").await?;
println!("Best sell: {:?}p", top.best_sell_price());
println!("Best buy: {:?}p", top.best_buy_price());

Managing Your Orders

use wf_market::{CreateOrder, UpdateOrder};

// Get your orders
let my_orders = client.my_orders().await?;

// Create a sell order
let order = client.create_order(
    CreateOrder::sell("nikana_prime_set", 100, 1).visible(true)
).await?;

// Update order price
client.update_order(
    order.id(),
    UpdateOrder::new().platinum(95)
).await?;

// Delete order
client.delete_order(order.id()).await?;

// Close order (record a sale)
let transaction = client.close_order(order.id(), 1).await?;

Type-Safe Order IDs

The OwnedOrderId type ensures you can only update/delete orders you own:

// This compiles - my_orders() returns OwnedOrder with OwnedOrderId
let orders = client.my_orders().await?;
client.delete_order(orders[0].id()).await?;

// This won't compile - get_orders() returns OrderListing with String id
let orders = client.get_orders("item").await?;
// client.delete_order(&orders[0].order.id); // Error!

Item Index

Items are automatically fetched when building a client. This enables O(1) item lookups and automatic item access from orders:

// Items loaded automatically on build()
let client = Client::builder().build().await?;

// Access items directly
println!("Total items: {}", client.items().len());

// O(1) lookups by ID or slug
if let Some(item) = client.get_item_by_slug("serration") {
    println!("Found: {}", item.name());
    
    // Check item type
    if item.is_mod() {
        println!("  Max rank: {}", item.as_mod().unwrap().max_rank());
    } else if item.is_regular() {
        println!("  Regular tradeable item");
    }
}

// Orders have direct item access
let orders = client.get_orders("nikana_prime_set").await?;
for order in &orders {
    if let Some(item) = order.get_item() {
        println!("{}: {}p", item.name(), order.order.platinum);
    }
}

Standalone Item Fetching

Fetch items without creating a client using ItemIndex::fetch(). This is useful for pre-loading items or building a client synchronously:

use wf_market::{Client, ItemIndex, Platform, Language};

// Fetch items independently (no client needed)
let index = ItemIndex::fetch().await?;
println!("Fetched {} items", index.len());

// Build client synchronously with pre-fetched items
let client = Client::builder().build_with_items(index);

// With custom platform/language settings
let index = ItemIndex::fetch_with_config(
    Platform::Playstation,
    Language::German,
    false, // crossplay
).await?;

Caching Items

For applications that restart frequently, use build_with_cache() to avoid re-fetching items:

use wf_market::ApiCache;

let mut cache = ApiCache::new();

// First build fetches from API, subsequent builds use cache (if < 1 day old)
let client = Client::builder().build_with_cache(&mut cache).await?;

// Cache is serializable for persistence across restarts
let json = serde_json::to_string(&cache.to_serializable())?;

Long-Running Applications

For long-running applications, refresh the item index periodically:

// Refresh items (new orders will use updated index)
client.revalidate_items().await?;

Caching

Use ApiCache for other endpoints that rarely change:

use wf_market::ApiCache;
use std::time::Duration;

let mut cache = ApiCache::new();

// First call fetches from API
let items = client.get_items(Some(&mut cache)).await?;

// Subsequent calls use cache (instant)
let items = client.get_items(Some(&mut cache)).await?;

// With TTL - refresh if older than 24 hours
let items = client.get_items_with_ttl(
    Some(&mut cache),
    Duration::from_secs(24 * 60 * 60),
).await?;

// Cache is serializable for persistence
let serializable = cache.to_serializable();
let json = serde_json::to_string(&serializable)?;

WebSocket (Real-time Updates)

Enable the websocket feature for real-time order updates:

[dependencies]
wf-market = { version = "0.2", features = ["websocket"] }
use wf_market::ws::{WsEvent, Subscription, WsUserStatus};

let ws = client.websocket()
    .on_event(|event| async move {
        match event {
            WsEvent::Connected => println!("Connected!"),
            WsEvent::OnlineCount { authorized, .. } => {
                println!("Users online: {}", authorized);
            }
            WsEvent::OrderCreated { order } => {
                println!("New order: {}p by {}", 
                    order.order.platinum, 
                    order.user.ingame_name
                );
            }
            _ => {}
        }
    })
    .subscribe(Subscription::all_new_orders())
    .auto_reconnect(true)
    .connect()
    .await?;

// Subscribe to specific items
ws.subscribe(Subscription::item("nikana_prime_set")).await?;

// Set your status
ws.set_status(WsUserStatus::Online, Some(3600), None).await?;

Item Types

Items can be categorized using type-checking methods:

let items = client.items();

for item in items.iter() {
    if item.is_mod() {
        // Mods have max rank
        let mod_view = item.as_mod().unwrap();
        println!("{}: max rank {}", item.name(), mod_view.max_rank());
    } else if item.is_sculpture() {
        // Ayatan sculptures have endo values
        let sculpture = item.as_sculpture().unwrap();
        println!("{}: {} endo", item.name(), sculpture.calculate_endo(None, None));
    } else if item.is_regular() {
        // Regular items (not mods, not sculptures)
        println!("{}: regular item", item.name());
    }
}

Mods

// Create mod order with rank
let order = CreateOrder::sell("serration", 50, 1)
    .with_mod_rank(10);

Ayatan Sculptures

if let Some(sculpture) = item.as_sculpture() {
    // Calculate endo with custom star counts
    let partial = sculpture.calculate_endo(Some(2), Some(1)); // 2 amber, 1 cyan
    let full = sculpture.calculate_endo(None, None); // fully socketed
    println!("{}: {} endo (partial) / {} endo (full)", item.name(), partial, full);
}

Configuration

use wf_market::{Client, ClientConfig, Platform, Language};

let config = ClientConfig {
    platform: Platform::Pc,
    language: Language::English,
    crossplay: true,
    rate_limit: 3, // requests per second
};

let client = Client::builder()
    .config(config)
    .build()
    .await?;

Feature Flags

Feature Default Description
rustls-tls Yes Use rustls for TLS
native-tls No Use native TLS instead of rustls
websocket No Enable WebSocket support for real-time updates
v1-api No Enable deprecated V1 API endpoints (statistics)

V1 API Endpoints (Deprecated)

Some endpoints are only available in the legacy V1 API. Enable them with the v1-api feature:

[dependencies]
wf-market = { version = "0.3", features = ["v1-api"] }

Warning: V1 endpoints are deprecated and will be removed when V2 equivalents become available.

Item Statistics

Get historical price and volume statistics for an item:

use wf_market::Client;

let client = Client::builder().build().await?;
let stats = client.get_item_statistics("nikana_prime_set").await?;

// Get recent average price from closed trades
if let Some(price) = stats.recent_avg_price() {
    println!("Recent average: {:.0}p", price);
}

// Analyze 48-hour trend (hourly data)
for entry in &stats.statistics_closed.hours_48 {
    if entry.volume > 0 {
        println!("{}: {:.0}p avg ({} trades)",
            entry.datetime.format("%H:%M"),
            entry.avg_price,
            entry.volume
        );
    }
}

// Check 90-day statistics
let total_volume = stats.statistics_closed.total_volume_90d();
let has_data = stats.has_sufficient_data();
println!("90-day volume: {} (sufficient data: {})", total_volume, has_data);

Examples

Run the examples with:

# Basic usage (no auth required)
cargo run --example basic_usage

# Authenticated examples (set WFM_EMAIL and WFM_PASSWORD)
export WFM_EMAIL="[email protected]"
export WFM_PASSWORD="your_password"

cargo run --example create_orders
cargo run --example session_persistence
cargo run --example websocket --features websocket

# Generate JWT token for testing (saves to .env)
cargo run --example update_token

Development

Running Tests

Unit tests run without credentials:

cargo test --all-features

Integration Tests

Integration tests require warframe.market credentials:

# 1. Set up credentials
cp .env.example .env
# Edit .env with your email and password

# 2. Generate a JWT token (avoids rate limiting)
cargo run --example update_token

# 3. Run integration tests (serially - server allows one WS connection)
cargo test --all-features -- --ignored --nocapture --test-threads=1

Troubleshooting:

  • "Rate limited": Wait 10-15 minutes, then run cargo run --example update_token
  • "JWT token expired": Run cargo run --example update_token to refresh
  • "Unknown error" on WebSocket: Use --test-threads=1 to run tests serially

Minimum Supported Rust Version

This crate requires Rust 1.85 or later (2024 edition).

License

This project is licensed under the GPL-3.0 License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Dependencies

~10–29MB
~372K SLoC