Audit of codex/porcelain-action-reliability, landing the changes that fix real defects and leaving the speculative parts behind. Interface options are published with a 1-based `index` label, but `sendClickInterfaceOption` takes a 0-based array position. `fletchLogs` and `craftLeather` matched an option by text and then fed its `index` straight into that method, so both crafted the product *after* the one requested — and matching the first option skipped it entirely. Adds `sdk.clickInterfaceOption(selector)`, which resolves by object or visible text and dispatches the option's `componentId`, and uses it at both call sites. `sendClickInterfaceOption` keeps its behaviour with a renamed parameter and a doc comment naming the trap. `buyFromShop`/`sellToShop` decomposed the requested amount into an array of 10/5/1 steps before sending anything, so `buyFromShop(item, 1e9)` allocated a hundred-million-element array. Quantities are now validated up front and the packets are streamed under a packet cap and deadline; `sellAllToShop`'s unbounded `while (true)` is bounded the same way. `sellToShop` reused the shop slot captured before the loop. Selling a batch of non-stackables removes that slot, so later packets sold whatever had slid into it. The slot is re-resolved by item id before each packet. Shop and bank quantity actions returned `success: true` for short fills, so a caller that asked for 28 proceeded believing it had 28. They now report `requestedAmount`, the amount that actually moved, and `partial: true`, and reserve `success` for an exact fill. `chopTree` returned success when the tree merely despawned — which happens when another player fells it — and then reported logs already in the inventory as the ones just chopped. Success now requires the log count to rise; the despawn still ends the wait, as a failure. `withdrawItem` identified the withdrawn item by diffing inventory slots, which names whatever landed in a changed slot rather than what was withdrawn. It now tracks the bank item's id. Also regenerates sdk/API.md, which was already stale on main (`bun run check` fails there at docs:api:check), and adds unit coverage for option resolution and quantity handling to test:focused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.7 KiB
Shops & Selling
Successful patterns for shop interactions and selling items.
CRITICAL: General Store Pricing (Major Discovery!)
General stores pay 0 GP when overstocked!
This was discovered the hard way after selling 40+ cow hides for 0 GP total. General stores have dynamic pricing based on stock levels:
| Stock Level | Price You Get |
|---|---|
| 0 (depleted) | Best price |
| Normal | Fair price |
| Overstocked | ~0 GP! |
Implication for Money-Making
Cow hides are worth ~100 GP each normally, but the Lumbridge general store pays 0 GP because it's completely overstocked (likely from other bots selling there).
Solutions:
- Find specialized shops (tanner, leather worker)
- Sell items the store actually needs (depleted stock)
- Use different money-making methods (mining, fishing sell for more)
Shop Locations
| Shop | Location | Coordinates | What They Sell |
|---|---|---|---|
| Lumbridge General Store | Lumbridge | (3212, 3247) | Basic supplies, tools |
| Varrock Sword Shop | Varrock | (3204, 3398) | Bronze to steel swords. Shop keeper (3203, 3397), Shop assistant (3205, 3399) |
| Bob's Axes | Lumbridge | (3230, 3203) | Axes (bronze 16gp), Pickaxes (bronze 1gp) |
| Lumbridge General Store | Lumbridge | (3210, 3244) | Hammer (1gp), Pot, Jug, Bucket, Tinderbox, Chisel, Shears |
| Gerrant's Fishy Business | Port Sarim | (3014, 3224) | Small fishing net (5gp), fishing gear |
Opening a Shop
// Find shopkeeper
const shopkeeper = sdk.getState()?.nearbyNpcs.find(n => /shopkeeper/i.test(n.name));
if (!shopkeeper) return;
// Find Trade option
const tradeOpt = shopkeeper.optionsWithIndex.find(o => /trade/i.test(o.text));
if (tradeOpt) {
await sdk.sendInteractNpc(shopkeeper.index, tradeOpt.opIndex);
}
// Wait for shop interface
for (let i = 0; i < 15; i++) {
await new Promise(r => setTimeout(r, 500));
if (sdk.getState()?.shop?.isOpen) {
console.log('Shop opened!');
break;
}
}
Selling Items
// Shop must be open first
if (!sdk.getState()?.shop?.isOpen) {
console.log('Shop not open!');
return;
}
// Find item to sell in inventory
const item = sdk.getState()?.inventory.find(i => /^cow hide$/i.test(i.name));
if (item) {
// Sell item (slot, quantity)
await sdk.sendShopSell(item.slot, item.count);
await new Promise(r => setTimeout(r, 200));
}
Buying Items
// Find item in shop stock
const shopItem = sdk.getState()?.shop?.shopItems?.find(i => /sword/i.test(i.name));
if (shopItem && shopItem.count > 0) {
await sdk.sendShopBuy(shopItem.slot, 1);
await new Promise(r => setTimeout(r, 200));
}
Partial Fills
bot.buyFromShop / bot.sellToShop report success: true only when the whole
requested amount went through. Running out of stock, coins, or inventory space
gives success: false with partial: true — read the amount rather than
assuming the request was filled:
const bought = await bot.buyFromShop(/bronze pickaxe/i, 5);
if (!bought.success) {
console.log(`Got ${bought.amountBought} of ${bought.requestedAmount} (${bought.reason})`);
}
Quantities above 1000 are rejected up front rather than expanded into thousands of Buy-1 packets.
Money-Making Alternatives
Since general stores are unreliable for selling, consider:
| Method | GP/Hour (approx) | Requirements |
|---|---|---|
| Pickpocketing men | ~50-100 GP | Thieving 1+ |
| Mining copper/tin | Variable | Mining 1+, pickaxe |
| Fishing shrimp | Variable | Fishing 1+, net |
Combat drops (bones, hides) are only valuable if you can find a specialized buyer or player to trade with.