Files
max bittkerandClaude Opus 5 9958f2b074 fix(sdk): correct interface-option clicks and make quantity actions honest
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>
2026-07-24 18:16:14 -04:00

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:

  1. Find specialized shops (tanner, leather worker)
  2. Sell items the store actually needs (depleted stock)
  3. 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.