Files
Operation-Blue-Laminate-v2/BlueLaminate/BlueLaminate.EFCore/Entities/CsMoneyListing.cs
bob dc7c3f99ae Add cs.money worker stack with per-worker IPRoyal residential proxy
Brings up the pull-model scraper: the .NET C2 hands skin+wear jobs to Python nodriver workers that scrape cs.money and post results back, plus the supporting Core/EFCore data model, migrations, and docker-compose orchestration.

IPRoyal proxying lets workers scale horizontally with a distinct residential exit IP each: every worker process mints its own sticky session at startup, and an in-process forwarding proxy injects the gateway auth so Chromium talks only to an auth-free localhost endpoint (zero CDP). On a Cloudflare challenge a worker rotates to a fresh session/IP and re-warms. Verified end-to-end against live IPRoyal: distinct US residential exits per worker and IP rotation on demand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:03:53 -05:00

68 lines
3.2 KiB
C#

namespace BlueLaminate.EFCore.Entities;
/// <summary>
/// One sell-order observed on cs.money via its internal
/// <c>GET /2.0/market/sell-orders</c> endpoint (scraped through the Python worker,
/// since cs.money has no public API and sits behind Cloudflare).
/// <para>
/// Kept in its own table rather than shared with the CSFloat <see cref="Listing"/>:
/// cs.money exposes a different shape (its own sell-order id, a pricing breakdown,
/// <c>quality</c>/phase, and no def/paint index). It still links to the
/// market-agnostic <see cref="SkinInstance"/> by fingerprint, so the same physical
/// item seen on both markets rolls up to one instance for cross-market analysis.
/// </para>
/// Soft-tracked across sweeps exactly like <see cref="Listing"/>:
/// <see cref="FirstSeenAt"/>/<see cref="LastSeenAt"/> bound the observation window
/// and <see cref="Status"/> flips to <see cref="ListingStatus.Removed"/> when a
/// once-seen order stops appearing (sold/delisted).
/// </summary>
public class CsMoneyListing
{
public int Id { get; set; }
/// <summary>cs.money's sell-order id (item.id). Natural key for dedup.</summary>
public long SellOrderId { get; set; }
/// <summary>
/// cs.money's asset id for the listed copy. Not a stable identity, but the
/// discriminator that distinguishes duped copies sharing one fingerprint.
/// </summary>
public string? AssetId { get; set; }
// Catalogue links. Unlike the CSFloat global sweep these are NOT best-effort:
// each scrape job targets one skin+wear, so the worker reports which Skin/
// Condition the results belong to and we set them directly.
public int SkinId { get; set; }
public Skin Skin { get; set; } = null!;
public int? ConditionId { get; set; }
public SkinCondition? Condition { get; set; }
/// <summary>The physical item (by fingerprint), shared with CSFloat listings.</summary>
public int? SkinInstanceId { get; set; }
public SkinInstance? SkinInstance { get; set; }
// Item identity, from the listing's asset block.
public string MarketHashName { get; set; } = null!;
public string? Quality { get; set; } // cs.money wear short code: fn/mw/ft/ww/bs
public decimal? FloatValue { get; set; } // null for non-skin items
public int? PaintSeed { get; set; } // asset.pattern
public string? Phase { get; set; } // doppler phase (sapphire/ruby/…)
public bool IsStatTrak { get; set; }
public bool IsSouvenir { get; set; }
public int StickerCount { get; set; }
// Pricing. cs.money returns a breakdown; Price is the actual asking price.
public decimal Price { get; set; } // pricing.default
public decimal? PriceBeforeDiscount { get; set; }
public decimal? ComputedPrice { get; set; } // pricing.computed (reference price)
public string Currency { get; set; } = "USD"; // cs.money returns no currency field
public string? InspectLink { get; set; }
// Soft-tracking across sweeps.
public DateTimeOffset FirstSeenAt { get; set; }
public DateTimeOffset LastSeenAt { get; set; }
public ListingStatus Status { get; set; }
public DateTimeOffset? RemovedAt { get; set; }
}