Skip to content

taker.config

taker.config

Configuration for JoinMarket Taker.

Classes

BroadcastPolicy

Bases: StrEnum

Policy for how to broadcast the final CoinJoin transaction.

Privacy implications: - SELF: Taker broadcasts via own node. Links taker's IP to the transaction (even via Tor). - RANDOM_PEER: Random maker selected. If verification fails, tries next maker, falls back to self as last resort. Good balance of privacy and reliability. - MULTIPLE_PEERS: Broadcast to N random makers simultaneously (default 3). Redundant and reliable without excessive network footprint. Falls back to self if all fail. - NOT_SELF: Try makers sequentially, never self. Maximum privacy - taker never broadcasts. WARNING: No fallback if all makers fail!

Neutrino considerations: - Neutrino cannot verify mempool transactions (only confirmed blocks) - MULTIPLE_PEERS is recommended and default: sends to multiple makers for redundancy - Self-fallback allowed but verification skipped (trusts broadcast succeeded)

Source code in taker/src/taker/config.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class BroadcastPolicy(StrEnum):
    """
    Policy for how to broadcast the final CoinJoin transaction.

    Privacy implications:
    - SELF: Taker broadcasts via own node. Links taker's IP to the transaction (even via Tor).
    - RANDOM_PEER: Random maker selected. If verification fails, tries next maker, falls back
                   to self as last resort. Good balance of privacy and reliability.
    - MULTIPLE_PEERS: Broadcast to N random makers simultaneously (default 3). Redundant and
                      reliable without excessive network footprint. Falls back to self if all fail.
    - NOT_SELF: Try makers sequentially, never self. Maximum privacy - taker never broadcasts.
                WARNING: No fallback if all makers fail!

    Neutrino considerations:
    - Neutrino cannot verify mempool transactions (only confirmed blocks)
    - MULTIPLE_PEERS is recommended and default: sends to multiple makers for redundancy
    - Self-fallback allowed but verification skipped (trusts broadcast succeeded)
    """

    SELF = "self"
    RANDOM_PEER = "random-peer"
    MULTIPLE_PEERS = "multiple-peers"
    NOT_SELF = "not-self"
Attributes
MULTIPLE_PEERS = 'multiple-peers' class-attribute instance-attribute
NOT_SELF = 'not-self' class-attribute instance-attribute
RANDOM_PEER = 'random-peer' class-attribute instance-attribute
SELF = 'self' class-attribute instance-attribute

MaxCjFee

Bases: BaseModel

Maximum CoinJoin fee limits.

Source code in taker/src/taker/config.py
39
40
41
42
43
class MaxCjFee(BaseModel):
    """Maximum CoinJoin fee limits."""

    abs_fee: int = Field(default=500, ge=0, description="Maximum absolute fee in sats")
    rel_fee: str = Field(default="0.001", description="Maximum relative fee (0.001 = 0.1%)")
Attributes
abs_fee: int = Field(default=500, ge=0, description='Maximum absolute fee in sats') class-attribute instance-attribute
rel_fee: str = Field(default='0.001', description='Maximum relative fee (0.001 = 0.1%)') class-attribute instance-attribute

Schedule

Bases: BaseModel

CoinJoin schedule for tumbler-style operations.

Source code in taker/src/taker/config.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
class Schedule(BaseModel):
    """CoinJoin schedule for tumbler-style operations."""

    entries: list[ScheduleEntry] = Field(default_factory=list)
    current_index: int = Field(default=0, ge=0)

    def current_entry(self) -> ScheduleEntry | None:
        """Get current schedule entry."""
        if self.current_index >= len(self.entries):
            return None
        return self.entries[self.current_index]

    def advance(self) -> bool:
        """Advance to next entry. Returns True if more entries remain."""
        if self.current_index < len(self.entries):
            self.entries[self.current_index].completed = True
            self.current_index += 1
        return self.current_index < len(self.entries)

    def is_complete(self) -> bool:
        """Check if all entries are complete."""
        return self.current_index >= len(self.entries)
Attributes
current_index: int = Field(default=0, ge=0) class-attribute instance-attribute
entries: list[ScheduleEntry] = Field(default_factory=list) class-attribute instance-attribute
Functions
advance() -> bool

Advance to next entry. Returns True if more entries remain.

Source code in taker/src/taker/config.py
227
228
229
230
231
232
def advance(self) -> bool:
    """Advance to next entry. Returns True if more entries remain."""
    if self.current_index < len(self.entries):
        self.entries[self.current_index].completed = True
        self.current_index += 1
    return self.current_index < len(self.entries)
current_entry() -> ScheduleEntry | None

Get current schedule entry.

Source code in taker/src/taker/config.py
221
222
223
224
225
def current_entry(self) -> ScheduleEntry | None:
    """Get current schedule entry."""
    if self.current_index >= len(self.entries):
        return None
    return self.entries[self.current_index]
is_complete() -> bool

Check if all entries are complete.

Source code in taker/src/taker/config.py
234
235
236
def is_complete(self) -> bool:
    """Check if all entries are complete."""
    return self.current_index >= len(self.entries)

ScheduleEntry

Bases: BaseModel

A single entry in a CoinJoin schedule.

Source code in taker/src/taker/config.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
class ScheduleEntry(BaseModel):
    """A single entry in a CoinJoin schedule."""

    mixdepth: int = Field(..., ge=0, le=9)
    amount: int | None = Field(
        default=None,
        ge=0,
        description="Amount in satoshis (mutually exclusive with amount_fraction)",
    )
    amount_fraction: float | None = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Fraction of balance (0.0-1.0, mutually exclusive with amount)",
    )
    counterparty_count: int = Field(..., ge=1, le=20)
    destination: str = Field(..., description="Destination address or 'INTERNAL'")
    wait_time: float = Field(default=0.0, ge=0.0, description="Wait time after completion")
    rounding: int = Field(default=16, ge=1, description="Significant figures for rounding")
    completed: bool = False

    @model_validator(mode="after")
    def validate_amount_fields(self) -> ScheduleEntry:
        """Ensure exactly one of amount or amount_fraction is set."""
        if self.amount is None and self.amount_fraction is None:
            raise ValueError("Must specify either 'amount' or 'amount_fraction'")
        if self.amount is not None and self.amount_fraction is not None:
            raise ValueError("Cannot specify both 'amount' and 'amount_fraction'")
        return self
Attributes
amount: int | None = Field(default=None, ge=0, description='Amount in satoshis (mutually exclusive with amount_fraction)') class-attribute instance-attribute
amount_fraction: float | None = Field(default=None, ge=0.0, le=1.0, description='Fraction of balance (0.0-1.0, mutually exclusive with amount)') class-attribute instance-attribute
completed: bool = False class-attribute instance-attribute
counterparty_count: int = Field(..., ge=1, le=20) class-attribute instance-attribute
destination: str = Field(..., description="Destination address or 'INTERNAL'") class-attribute instance-attribute
mixdepth: int = Field(..., ge=0, le=9) class-attribute instance-attribute
rounding: int = Field(default=16, ge=1, description='Significant figures for rounding') class-attribute instance-attribute
wait_time: float = Field(default=0.0, ge=0.0, description='Wait time after completion') class-attribute instance-attribute
Functions
validate_amount_fields() -> ScheduleEntry

Ensure exactly one of amount or amount_fraction is set.

Source code in taker/src/taker/config.py
205
206
207
208
209
210
211
212
@model_validator(mode="after")
def validate_amount_fields(self) -> ScheduleEntry:
    """Ensure exactly one of amount or amount_fraction is set."""
    if self.amount is None and self.amount_fraction is None:
        raise ValueError("Must specify either 'amount' or 'amount_fraction'")
    if self.amount is not None and self.amount_fraction is not None:
        raise ValueError("Cannot specify both 'amount' and 'amount_fraction'")
    return self

TakerConfig

Bases: WalletConfig

Configuration for taker bot.

Inherits base wallet configuration from jmcore.config.WalletConfig and adds taker-specific settings for CoinJoin execution, PoDLE, and broadcasting.

Source code in taker/src/taker/config.py
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
class TakerConfig(WalletConfig):
    """
    Configuration for taker bot.

    Inherits base wallet configuration from jmcore.config.WalletConfig
    and adds taker-specific settings for CoinJoin execution, PoDLE,
    and broadcasting.
    """

    # CoinJoin settings
    destination_address: SecretStr = Field(
        default_factory=lambda: SecretStr(""),
        description="Target address for CJ output, empty = INTERNAL",
    )
    amount: int = Field(default=0, ge=0, description="Amount in sats (0 = sweep)")
    mixdepth: int = Field(default=0, ge=0, description="Source mixdepth")
    counterparty_count: int = Field(
        default=10, ge=1, le=20, description="Number of makers to select"
    )

    # Fee settings
    max_cj_fee: MaxCjFee = Field(
        default_factory=MaxCjFee, description="Maximum CoinJoin fee limits"
    )
    tx_fee_factor: float = Field(
        default=0.2,
        ge=0.0,
        description="Randomization factor for fees (randomized between base and base*(1+factor))",
    )
    fee_rate: float | None = Field(
        default=None,
        gt=0.0,
        description="Manual fee rate in sat/vB (mutually exclusive with fee_block_target)",
    )
    fee_block_target: int | None = Field(
        default=None,
        ge=1,
        le=1008,
        description="Target blocks for fee estimation (mutually exclusive with fee_rate). "
        "Defaults to 3 when connected to full node.",
    )
    bondless_makers_allowance: float = Field(
        default=0.0,
        ge=0.0,
        le=1.0,
        description="Fraction of time to choose makers randomly (not by fidelity bond)",
    )
    bond_value_exponent: float = Field(
        default=1.3,
        gt=0.0,
        description="Exponent for fidelity bond value calculation (default 1.3)",
    )
    bondless_makers_allowance_require_zero_fee: bool = Field(
        default=True,
        description="For bondless maker spots, require zero absolute fee (percentage fee OK)",
    )

    # PoDLE settings
    taker_utxo_retries: int = Field(
        default=3,
        ge=1,
        le=10,
        description="Maximum PoDLE index retries per UTXO (reference: 3)",
    )
    taker_utxo_age: int = Field(default=5, ge=1, description="Minimum UTXO confirmations")
    taker_utxo_amtpercent: int = Field(
        default=20, ge=1, le=100, description="Min UTXO value as % of CJ amount"
    )

    # Timeouts
    maker_timeout_sec: int = Field(default=60, ge=10, description="Timeout for maker responses")
    order_wait_time: float = Field(
        default=120.0,
        ge=1.0,
        description=(
            "Seconds to wait for orderbook responses. Empirical testing shows 95th "
            "percentile response time over Tor is ~101s. Default 120s (with 20% buffer) "
            "captures ~95% of offers."
        ),
    )

    # Broadcast policy (privacy vs reliability tradeoff)
    tx_broadcast: BroadcastPolicy = Field(
        default=BroadcastPolicy.MULTIPLE_PEERS,
        description="How to broadcast: self, random-peer, multiple-peers, or not-self",
    )
    broadcast_timeout_sec: int = Field(
        default=30,
        ge=5,
        description="Timeout waiting for maker to broadcast when delegating",
    )
    broadcast_peer_count: int = Field(
        default=3,
        ge=1,
        description="Number of random peers to use for MULTIPLE_PEERS policy",
    )

    # Advanced options
    preferred_offer_type: OfferType = Field(
        default=OfferType.SW0_RELATIVE, description="Preferred offer type"
    )
    minimum_makers: int = Field(default=1, ge=1, description="Minimum number of makers required")
    max_maker_replacement_attempts: int = Field(
        default=3,
        ge=0,
        le=10,
        description="Max attempts to replace non-responsive makers (0 = disabled)",
    )
    select_utxos: bool = Field(
        default=False,
        description="Interactively select UTXOs before CoinJoin (CLI only)",
    )

    # Wallet rescan configuration
    rescan_interval_sec: int = Field(
        default=600,
        ge=60,
        description="Interval in seconds for periodic wallet rescans (default: 10 minutes)",
    )

    @model_validator(mode="after")
    def set_bitcoin_network_default(self) -> TakerConfig:
        """If bitcoin_network is not set, default to the protocol network."""
        if self.bitcoin_network is None:
            object.__setattr__(self, "bitcoin_network", self.network)
        return self

    @model_validator(mode="after")
    def validate_fee_options(self) -> TakerConfig:
        """Ensure fee_rate and fee_block_target are mutually exclusive."""
        if self.fee_rate is not None and self.fee_block_target is not None:
            raise ValueError(
                "Cannot specify both fee_rate and fee_block_target. "
                "Use fee_rate for manual rate, or fee_block_target for estimation."
            )
        return self
Attributes
amount: int = Field(default=0, ge=0, description='Amount in sats (0 = sweep)') class-attribute instance-attribute
bond_value_exponent: float = Field(default=1.3, gt=0.0, description='Exponent for fidelity bond value calculation (default 1.3)') class-attribute instance-attribute
bondless_makers_allowance: float = Field(default=0.0, ge=0.0, le=1.0, description='Fraction of time to choose makers randomly (not by fidelity bond)') class-attribute instance-attribute
bondless_makers_allowance_require_zero_fee: bool = Field(default=True, description='For bondless maker spots, require zero absolute fee (percentage fee OK)') class-attribute instance-attribute
broadcast_peer_count: int = Field(default=3, ge=1, description='Number of random peers to use for MULTIPLE_PEERS policy') class-attribute instance-attribute
broadcast_timeout_sec: int = Field(default=30, ge=5, description='Timeout waiting for maker to broadcast when delegating') class-attribute instance-attribute
counterparty_count: int = Field(default=10, ge=1, le=20, description='Number of makers to select') class-attribute instance-attribute
destination_address: SecretStr = Field(default_factory=(lambda: SecretStr('')), description='Target address for CJ output, empty = INTERNAL') class-attribute instance-attribute
fee_block_target: int | None = Field(default=None, ge=1, le=1008, description='Target blocks for fee estimation (mutually exclusive with fee_rate). Defaults to 3 when connected to full node.') class-attribute instance-attribute
fee_rate: float | None = Field(default=None, gt=0.0, description='Manual fee rate in sat/vB (mutually exclusive with fee_block_target)') class-attribute instance-attribute
maker_timeout_sec: int = Field(default=60, ge=10, description='Timeout for maker responses') class-attribute instance-attribute
max_cj_fee: MaxCjFee = Field(default_factory=MaxCjFee, description='Maximum CoinJoin fee limits') class-attribute instance-attribute
max_maker_replacement_attempts: int = Field(default=3, ge=0, le=10, description='Max attempts to replace non-responsive makers (0 = disabled)') class-attribute instance-attribute
minimum_makers: int = Field(default=1, ge=1, description='Minimum number of makers required') class-attribute instance-attribute
mixdepth: int = Field(default=0, ge=0, description='Source mixdepth') class-attribute instance-attribute
order_wait_time: float = Field(default=120.0, ge=1.0, description='Seconds to wait for orderbook responses. Empirical testing shows 95th percentile response time over Tor is ~101s. Default 120s (with 20% buffer) captures ~95% of offers.') class-attribute instance-attribute
preferred_offer_type: OfferType = Field(default=(OfferType.SW0_RELATIVE), description='Preferred offer type') class-attribute instance-attribute
rescan_interval_sec: int = Field(default=600, ge=60, description='Interval in seconds for periodic wallet rescans (default: 10 minutes)') class-attribute instance-attribute
select_utxos: bool = Field(default=False, description='Interactively select UTXOs before CoinJoin (CLI only)') class-attribute instance-attribute
taker_utxo_age: int = Field(default=5, ge=1, description='Minimum UTXO confirmations') class-attribute instance-attribute
taker_utxo_amtpercent: int = Field(default=20, ge=1, le=100, description='Min UTXO value as % of CJ amount') class-attribute instance-attribute
taker_utxo_retries: int = Field(default=3, ge=1, le=10, description='Maximum PoDLE index retries per UTXO (reference: 3)') class-attribute instance-attribute
tx_broadcast: BroadcastPolicy = Field(default=(BroadcastPolicy.MULTIPLE_PEERS), description='How to broadcast: self, random-peer, multiple-peers, or not-self') class-attribute instance-attribute
tx_fee_factor: float = Field(default=0.2, ge=0.0, description='Randomization factor for fees (randomized between base and base*(1+factor))') class-attribute instance-attribute
Functions
set_bitcoin_network_default() -> TakerConfig

If bitcoin_network is not set, default to the protocol network.

Source code in taker/src/taker/config.py
166
167
168
169
170
171
@model_validator(mode="after")
def set_bitcoin_network_default(self) -> TakerConfig:
    """If bitcoin_network is not set, default to the protocol network."""
    if self.bitcoin_network is None:
        object.__setattr__(self, "bitcoin_network", self.network)
    return self
validate_fee_options() -> TakerConfig

Ensure fee_rate and fee_block_target are mutually exclusive.

Source code in taker/src/taker/config.py
173
174
175
176
177
178
179
180
181
@model_validator(mode="after")
def validate_fee_options(self) -> TakerConfig:
    """Ensure fee_rate and fee_block_target are mutually exclusive."""
    if self.fee_rate is not None and self.fee_block_target is not None:
        raise ValueError(
            "Cannot specify both fee_rate and fee_block_target. "
            "Use fee_rate for manual rate, or fee_block_target for estimation."
        )
    return self