Bounties¶
Bounties turn the personal-reputation system into a hunt-and-collect loop. They give every player on the server a financial incentive to take down the worst offenders, and they give wronged players a way to put a real price tag on revenge.
A bounty is an amount of credits attached to a target. Anyone who lands the killing blow against that target in ship-vs-ship combat collects the entire pot.
Two kinds of bounty¶
| Kind | Created by | Stored where | Refundable |
|---|---|---|---|
| Player-placed | Any player paying the placement fee | Player.settings["bounties"] JSONB list (on the target) |
Yes (principal only) |
| System | Auto-attached by the Federation Bounty Board based on the target's personal_reputation |
Computed on demand from reputation tier | n/a |
System bounties never live in the database — they are derived by reading the target's personal_reputation and looking up SYSTEM_BOUNTY_TIERS. Player-placed bounties are persistent JSONB entries on the target.
✅ Shipped — service-layer bounty placement, system-tier derivation, collection on kill, REST endpoints, websocket events.
🚧 Partial — admin tooling for cancelling stuck bounties on deleted targets, and a soft cap that auto-collapses many small placements.
📐 Design-only — faction-issued bounties (Federation putting a bounty on a specific pirate captain that pays out only to faction members).
Placing a bounty¶
A player calls POST /api/v1/bounty/place with {target_id, amount}. The placer pays amount + 10% up front:
amount >= 1000 credits (BOUNTY_MIN_AMOUNT)
fee = floor(amount * 0.10) (non-refundable)
total_cost = amount + fee
placer.credits -= total_cost
target.settings["bounties"].append({
id, placed_by, placed_by_name,
amount, placed_at, type: "player"
})
Self-bounties are rejected. The placer's row is row-locked during placement to keep two simultaneous placements from double-spending the same credits. ✅ Shipped.
A bounty_placed event is emitted on the realtime bus to the placer, the target, and any global bounty-board subscribers.
System bounty tiers¶
The Federation auto-attaches a bounty when a player's personal_reputation is deeply negative. Only the single highest-tier bounty applies (the deepest pit pays out, lower-tier bounties don't stack on top of it).
personal_reputation ≤ |
System bounty |
|---|---|
| −500 | 5,000 |
| −750 | 25,000 |
| −1,000 | 100,000 |
System bounties materialise and disappear automatically as reputation crosses thresholds — no scheduler involved. ✅ Shipped (service computes them per-call).
🐛 Bug — current code accumulates all matched tiers for the same target instead of taking only the deepest. Target spec is single-tier; the loop in _get_system_bounties should be replaced with a max-match.
Collection on kill¶
When the combat resolver finishes a ship-vs-ship fight with a clear winner (see combat.md), it calls BountyService.collect_bounty(collector_id, target_id):
- Lock both player rows.
- Sum player-placed bounties on target.
- Sum system bounties (computed from reputation).
- Credit the collector with the full total.
- Clear
target.settings["bounties"](player-placed only — system bounties go away when the target's reputation recovers). - Emit
bounty_collectedto the collector, the target, and globally.
System-bounty payouts come from the Federation treasury; the model treats them as a pure credit source (the Federation is not modeled as a wallet). Player-placed payouts come from the escrow held in each placement.
✅ Shipped.
Cancellation¶
The placer can cancel an uncollected bounty:
DELETE /api/v1/bounty/{bounty_id}
1. find entry by bounty_id in target.settings["bounties"]
2. verify placed_by == placer.id
3. refund amount (NOT the 10% fee) to placer.credits
4. remove entry from list
5. emit bounty_cancelled
The 10% fee is gone forever. Cancellation is intended as a soft escape hatch, not a frictionless undo. ✅ Shipped.
Expiration¶
Bounties do not auto-expire. They sit on the target until collected or cancelled.
📐 Design-only — optional expiry timestamp on placement (expires_at); auto-refund-minus-fee on expiry. Useful for time-pressure events (e.g., a 48-hour vendetta).
Browsing bounties¶
GET /api/v1/bounty/board returns the top-N targets ranked by total bounty value (player + system combined), filtered to Player.is_active = true. Result shape per target:
{
"player_id": "...",
"player_name": "...",
"reputation_tier": "Villain",
"total_bounty": 137000,
"bounty_count": 4,
"current_sector": 442
}
This is the public bounty board. ✅ Shipped (service method get_available_bounties).
🚧 Partial — UI surface in the player client (a styled board with portraits, last-seen sector, recent kill log).
Reputation interactions¶
Bounties and reputation are tightly coupled:
- Killing a player who had bounties awards
+100reputation (defeat_bounty_target) — bounty hunting is heroic. - Killing a player who had no bounty (an "innocent") costs
−100reputation (attack_innocent). - Killing a player in an escape pod costs
−500reputation regardless of bounty status (kill_escape_pod). - Defending against an attacker awards
+50(defend_against_attacker).
Drift to personal_reputation ≤ −500 automatically attaches a system bounty, so attacking innocents and pod-killers are self-targeting — they place a bounty on themselves the more they offend.
See SYSTEMS/bounty-and-reputation.md for the full reputation tier table and combat-hook contract.
Edge cases¶
| Case | Behaviour |
|---|---|
| Two attackers fight the same target simultaneously | Combat resolver determines a single winner; collect_bounty runs once for that winner. Both rows row-locked during the call. |
| Target has zero bounties when killed | collect_bounty returns {success: false}; combat result is unaffected. |
| Bounty placed on a player who deletes their account | Placer can refund via admin tool. 🚧 Partial. |
| Bounty list grows unbounded (50+ small placements on one target) | Soft cap at 50 entries; older entries collapsed by placer (sum amounts). 📐 Design-only. |
| Reputation recovers after system bounty was attached | System bounty disappears the moment personal_reputation rises above the threshold (computed live). |
Source map¶
| Concern | Path (target) |
|---|---|
| Bounty service | services/gameserver/src/services/bounty_service.py |
| Reputation service (deltas) | services/gameserver/src/services/personal_reputation_service.py |
Combat hook calling collect_bounty |
services/gameserver/src/services/combat_service.py |
| Player JSONB field | services/gameserver/src/models/player.py (settings["bounties"]) |
| REST routes | services/gameserver/src/api/routes/bounty.py (target — currently in player.py) |
| Realtime events | services/gameserver/src/services/websocket_service.py |
Related¶
combat.md— kill resolution and the combat-side hook.factions-and-teams.md— how reputation and faction standing differ.SYSTEMS/bounty-and-reputation.md— full subsystem spec including reputation tiers, decay, and invariants.SYSTEMS/combat-resolver.md— where the kill detection that triggers collection lives.