rentahuman-mcp 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -211,9 +211,11 @@ worker (`escrow_recipient_mismatch`).
211
211
  To target every eligible worker in one country, pass `location` with the ISO
212
212
  country code and `isRemoteAllowed: false`, omitting `city` and `state`. The
213
213
  same shape works with `update_bounty`. Platform-blocked countries are rejected.
214
+ To open one bounty to several countries, pass `supportedCountries` instead.
214
215
 
215
216
  ```text
216
217
  location: { country: "US", isRemoteAllowed: false }
218
+ supportedCountries: ["US", "CA", "GB", "NZ", "AU"]
217
219
  ```
218
220
 
219
221
  For ordinary accepted one-shot bounties, the worker marks the task complete by
@@ -307,6 +309,32 @@ Only workers whose profile country matches can apply or receive automatic
307
309
  outreach. The country gate still applies when remote work is allowed. Include a
308
310
  `city` for city-level targeting instead.
309
311
 
312
+ To recruit across several countries, pass `supportedCountries` with the ISO
313
+ codes (country names are recognized too). Omit `location.country`, or keep it
314
+ and make sure it appears in the list:
315
+
316
+ ```json
317
+ {
318
+ "location": { "isRemoteAllowed": true },
319
+ "supportedCountries": ["US", "CA", "GB", "NZ", "AU"],
320
+ "identityRequired": true
321
+ }
322
+ ```
323
+
324
+ The list is normalized to uppercase ISO codes and deduplicated; blocked or
325
+ unrecognized countries are rejected with a `400`. With `identityRequired`, the
326
+ list is enforced against the applicant's verified document country. Use
327
+ `isRemoteAllowed: true`: an on-site bounty (`isRemoteAllowed: false`) with a
328
+ location country is gated by that country alone and accepts no other list. On
329
+ `update_bounty`, send the full new list to change it. Omitting it leaves an
330
+ explicit multi-country list in place, so editing `location` alone never drops
331
+ it; legacy bounties that only mirror a country-only `location` keep deriving
332
+ that single country from `location`. An empty array is stored as an explicit
333
+ empty list; it never lifts a country-only or on-site country restriction
334
+ (remove `location.country` first), and a remote city location with a country
335
+ stays unrestricted as before. `aiManaged` bounties do not accept
336
+ `supportedCountries`.
337
+
310
338
  #### Completion deadline (auto-reassign)
311
339
 
312
340
  The bounty-level `deadline` is the application cutoff. At or after that time,
package/dist/serve.js CHANGED
@@ -1326,6 +1326,7 @@ var agentCheckoutTools = [
1326
1326
  var BOUNTY_PRICE_TYPES = ["fixed", "hourly"];
1327
1327
  var BOUNTY_MIN_PRICE_USD = 3;
1328
1328
  var MAX_EXCLUDED_PARTICIPANT_SOURCE_BOUNTIES = 1e3;
1329
+ var MAX_BOUNTY_SUPPORTED_COUNTRIES = 250;
1329
1330
  var HUMANIZATION_FORMATS = ["text"];
1330
1331
  var HUMANIZATION_TRANSFORMATIONS = [
1331
1332
  "paraphrase",
@@ -1480,6 +1481,11 @@ var BountyLocationRequest = Schema5.Struct({
1480
1481
  isRemoteAllowed: Schema5.optional(Schema5.Boolean)
1481
1482
  });
1482
1483
  var BOUNTY_LOCATION_DESCRIPTION = "Worker location target. Set country to an ISO country code without city or state to target every eligible worker in that country, including when remote work is allowed. Include city (and optionally state/country) for city-level targeting. Platform-blocked countries are rejected.";
1484
+ var BountySupportedCountriesRequest = Schema5.Array(
1485
+ Schema5.String.pipe(Schema5.minLength(1), Schema5.maxLength(100))
1486
+ ).pipe(Schema5.maxItems(MAX_BOUNTY_SUPPORTED_COUNTRIES));
1487
+ var BOUNTY_SUPPORTED_COUNTRIES_CREATE_DESCRIPTION = 'Countries whose workers may see and apply, as ISO country codes (country names are also recognized), e.g. ["US", "CA", "GB", "NZ", "AU"]. Omit it to keep the location-derived policy: a country-only location restricts the bounty to that country, an on-site city location keeps its existing country gate, and other locations stay unrestricted. When location.country is set it must appear in this list, and on-site bounties (isRemoteAllowed false) with a location country accept only that one country; use isRemoteAllowed true for a remote multi-country bounty. Platform-blocked countries and unrecognized values are rejected. With identityRequired, the list is enforced against the verified document country. Not accepted on aiManaged bounties.';
1488
+ var BOUNTY_SUPPORTED_COUNTRIES_UPDATE_DESCRIPTION = 'Replace the eligible-country list with the full new list of ISO country codes (country names are also recognized), e.g. ["US", "CA", "GB", "NZ", "AU"]. Omitting it leaves an explicit multi-country list in place, so a location-only edit never drops it; legacy bounties that only mirror a country-only location keep deriving that single country from location. On-site bounties (isRemoteAllowed false) with a location country accept only that one country. An empty array is stored as an explicit empty list; it never lifts a country-only or on-site country restriction (remove location.country first), while a remote city location with a country stays unrestricted as before. When location.country is set it must appear in the list. Platform-blocked countries and unrecognized values are rejected.';
1483
1489
  var LIFECYCLE_MESSAGES_DESCRIPTION = "Auto-sent to the doer in the bounty conversation on lifecycle transitions. Supports {{name}}, {{bountyTitle}}, {{deadline}}, {{reason}} interpolation. Accepted/submission templates are only ever delivered to accepted applicants.";
1484
1490
  var LifecycleMessageTemplateSchema = Schema5.String.pipe(
1485
1491
  Schema5.maxLength(BOUNTY_LIFECYCLE_TEMPLATE_MAX_LENGTH)
@@ -1627,6 +1633,11 @@ var CreateBountyRequest = Schema5.Struct({
1627
1633
  location: Schema5.optional(BountyLocationRequest).annotations({
1628
1634
  description: BOUNTY_LOCATION_DESCRIPTION
1629
1635
  }),
1636
+ supportedCountries: Schema5.optional(
1637
+ BountySupportedCountriesRequest
1638
+ ).annotations({
1639
+ description: BOUNTY_SUPPORTED_COUNTRIES_CREATE_DESCRIPTION
1640
+ }),
1630
1641
  deadline: Schema5.optional(Schema5.String).annotations({
1631
1642
  description: "Application cutoff (ISO 8601 format). At or after this time, the listing is removed from discovery and no new applications or direct uploads are accepted. Use completionWindowHours for a post-acceptance completion deadline."
1632
1643
  }),
@@ -1892,6 +1903,11 @@ var UpdateBountyRequest = Schema5.Struct({
1892
1903
  location: Schema5.optional(BountyLocationRequest).annotations({
1893
1904
  description: BOUNTY_LOCATION_DESCRIPTION
1894
1905
  }),
1906
+ supportedCountries: Schema5.optional(
1907
+ BountySupportedCountriesRequest
1908
+ ).annotations({
1909
+ description: BOUNTY_SUPPORTED_COUNTRIES_UPDATE_DESCRIPTION
1910
+ }),
1895
1911
  status: Schema5.optional(
1896
1912
  Schema5.Literal("open", "in_review", "paused", "closed")
1897
1913
  ).annotations({
@@ -2383,7 +2399,7 @@ var CancelBountyRequest = BountyCancellationFeedbackRequest.pipe(
2383
2399
  );
2384
2400
  var createBountySpec = {
2385
2401
  name: "create_bounty",
2386
- description: `Create a one-shot task bounty for humans to apply to. Dry-run preview.fundingTotal is the total funding requirement before any existing wallet balance is applied; show that estimate to the operator before posting. Live pending_deposit responses include checkout_total, the exact hosted charge after wallet balance is applied, next to deposit_url. To target an entire country, pass location with an ISO country code and isRemoteAllowed=false while omitting city and state. Standard application bounties support application detail items via applicationDetails, including text questions, optional or required acknowledgment checkboxes, one-file uploads (allowedFileTypes: image, docx, pdf, txt, video, audio), and one required live_video field whose label is the script applicants record with the in-browser camera. For appearance-based bounties where the worker appears on camera or their presence is part of the deliverable (sign holding, UGC, sponsored posts, on-camera video, promo/GTM appearances), always include one required live_video field with a label asking the applicant to explain in two sentences why they are a good fit. Set liveCaptureRequirement to photo or video when accepted workers must capture new camera evidence after acceptance; neither camera-only workflow is cryptographic liveness verification. Do not ask for passwords, OTP/2FA codes, API keys, private keys, seed phrases, government IDs, bank/card details, exact home addresses, dates of birth, or other sensitive personal information. Blank rows are ignored; uploads are capped at 3 fields, acknowledgments at 5 fields, and live video at 1 field. Direct-review photo, video, or document collection bounties use submissionMode and matching upload settings instead of applicationDetails. lifecycleMessages can define auto-message templates for acceptance, rejection, and submission review transitions. Set completionWindowHours (any whole number of hours from 1 to 720, e.g. 36) to give confirmed workers a completion deadline: overdue seats are automatically released and reopened for other applicants (auto-reassign), workers can request extensions, and you approve/deny/grant them per worker. Pass optional \`idempotencyKey\` to make this safe to retry (a replayed key returns the original result instead of duplicating). Applicants are auto-reviewed by default (autoAccept, default true): deterministic checks always run and an AI review runs only for free-text screening answers \u2014 qualified applicants are accepted, clear mismatches rejected with a reason, uncertain cases left pending for manual review; pass autoAccept: false to review and accept every application yourself; on micCheckRequired bounties, set autoAcceptMinMicScore (1-5) to make the mic score a hard cutoff: applications below it are auto-rejected, at/above proceed toward acceptance, and unscored recordings stay pending. When omitted, the platform default 3.0 applies and lower scores only stay pending for manual review. To keep previous participants out, pass excludedParticipantSourceBountyIds with up to ${MAX_EXCLUDED_PARTICIPANT_SOURCE_BOUNTIES} ids of your OWN earlier bounties: any worker who was ever accepted on one of them cannot see or apply to the new bounty, and you can change the list later with update_bounty. AI-managed, QA, and taste run bounties, and never-funded drafts are rejected as sources. BETA: pass aiManaged: true to have the platform fully manage the bounty \u2014 automated recruiting, vetting, submission review, payment release, and a final report. Fixed price USD only, 1-50 spots. Poll the bounty's managed run or subscribe to run.report_ready for the finished work.`,
2402
+ description: `Create a one-shot task bounty for humans to apply to. Dry-run preview.fundingTotal is the total funding requirement before any existing wallet balance is applied; show that estimate to the operator before posting. Live pending_deposit responses include checkout_total, the exact hosted charge after wallet balance is applied, next to deposit_url. To target an entire country, pass location with an ISO country code and isRemoteAllowed=false while omitting city and state. To open one bounty to several countries, pass supportedCountries with the ISO codes (e.g. ["US", "CA", "GB", "NZ", "AU"]); when location.country is set it must be in that list, on-site bounties (isRemoteAllowed=false) accept only their location country, and with identityRequired the list is enforced against the verified document country. Standard application bounties support application detail items via applicationDetails, including text questions, optional or required acknowledgment checkboxes, one-file uploads (allowedFileTypes: image, docx, pdf, txt, video, audio), and one required live_video field whose label is the script applicants record with the in-browser camera. For appearance-based bounties where the worker appears on camera or their presence is part of the deliverable (sign holding, UGC, sponsored posts, on-camera video, promo/GTM appearances), always include one required live_video field with a label asking the applicant to explain in two sentences why they are a good fit. Set liveCaptureRequirement to photo or video when accepted workers must capture new camera evidence after acceptance; neither camera-only workflow is cryptographic liveness verification. Do not ask for passwords, OTP/2FA codes, API keys, private keys, seed phrases, government IDs, bank/card details, exact home addresses, dates of birth, or other sensitive personal information. Blank rows are ignored; uploads are capped at 3 fields, acknowledgments at 5 fields, and live video at 1 field. Direct-review photo, video, or document collection bounties use submissionMode and matching upload settings instead of applicationDetails. lifecycleMessages can define auto-message templates for acceptance, rejection, and submission review transitions. Set completionWindowHours (any whole number of hours from 1 to 720, e.g. 36) to give confirmed workers a completion deadline: overdue seats are automatically released and reopened for other applicants (auto-reassign), workers can request extensions, and you approve/deny/grant them per worker. Pass optional \`idempotencyKey\` to make this safe to retry (a replayed key returns the original result instead of duplicating). Applicants are auto-reviewed by default (autoAccept, default true): deterministic checks always run and an AI review runs only for free-text screening answers \u2014 qualified applicants are accepted, clear mismatches rejected with a reason, uncertain cases left pending for manual review; pass autoAccept: false to review and accept every application yourself; on micCheckRequired bounties, set autoAcceptMinMicScore (1-5) to make the mic score a hard cutoff: applications below it are auto-rejected, at/above proceed toward acceptance, and unscored recordings stay pending. When omitted, the platform default 3.0 applies and lower scores only stay pending for manual review. To keep previous participants out, pass excludedParticipantSourceBountyIds with up to ${MAX_EXCLUDED_PARTICIPANT_SOURCE_BOUNTIES} ids of your OWN earlier bounties: any worker who was ever accepted on one of them cannot see or apply to the new bounty, and you can change the list later with update_bounty. AI-managed, QA, and taste run bounties, and never-funded drafts are rejected as sources. BETA: pass aiManaged: true to have the platform fully manage the bounty \u2014 automated recruiting, vetting, submission review, payment release, and a final report. Fixed price USD only, 1-50 spots. Poll the bounty's managed run or subscribe to run.report_ready for the finished work.`,
2387
2403
  input: CreateBountyRequest
2388
2404
  };
2389
2405
  var getBountySpec = {
@@ -2424,7 +2440,7 @@ var decideExtensionSpec = {
2424
2440
  };
2425
2441
  var updateBountySpec = {
2426
2442
  name: "update_bounty",
2427
- description: `Update ordinary one-shot bounty details. You can modify the title, description, price, application-cutoff deadline, location, requiredLinks, applicationDetails, lifecycleMessages, liveCaptureRequirement, reactivate hidden inactive bounties, and more. The deadline stops new applications and direct uploads; completionWindowHours controls post-acceptance completion timing. To target an entire country, pass location with an ISO country code and isRemoteAllowed=false while omitting city and state. Live capture and required-link requirements can only be changed before applications are received. applicationDetails may be changed after applications are received; existing applications keep their original answers, while future applications use the latest fields. applicationDetails are application detail items for standard application bounties only; blank rows are ignored, uploads are capped at 3 fields, acknowledgments at 5 fields, and camera-only live_video at 1 required field. lifecycleMessages can define auto-message templates for acceptance, rejection, and submission review transitions. Admin-only ongoing bounty settings are intentionally not exposed through MCP. You can also pause/unpause a bounty (status 'paused' stops new applications and outreach, including on a fully staffed bounty; status 'open' resumes it, and the resumed status is derived from its seats: open, partially_filled, or assigned when every seat is still filled), close an unassigned bounty and return its unused funding (status 'closed'), increase seats via spotsAvailable, keep pending applicants on fill via keepApplicantsOnFill, and change the auto-reassign completion deadline via completionWindowHours (any whole number of hours from 1 to 720; null disables; only affects seats confirmed after the edit), and toggle automatic applicant review via autoAccept (applications received while off stay pending; toggling on affects future applications only; ignored on aiManaged bounties) or adjust its mic-quality cutoff via autoAcceptMinMicScore (1-5 \u2014 applications scoring below your configured cutoff are auto-rejected, at/above proceed; null resets to the 3.0 default, below which applications only stay pending). The exclude-previous-participants list can be changed after posting: pass excludedParticipantSourceBountyIds with the full new list of up to ${MAX_EXCLUDED_PARTICIPANT_SOURCE_BOUNTIES} ids of your OWN earlier bounties (an empty array clears it); the same source rules as create_bounty apply, and a bounty can never exclude itself. Use cancel_bounty for cancellation and refund handling. Work completion and payment are system-managed from escrow and payout evidence; status 'completed' and 'paid' cannot be set directly.`,
2443
+ description: `Update ordinary one-shot bounty details. You can modify the title, description, price, application-cutoff deadline, location, requiredLinks, applicationDetails, lifecycleMessages, liveCaptureRequirement, reactivate hidden inactive bounties, and more. The deadline stops new applications and direct uploads; completionWindowHours controls post-acceptance completion timing. To target an entire country, pass location with an ISO country code and isRemoteAllowed=false while omitting city and state. To change which countries may apply, pass supportedCountries with the full new list of ISO codes; omitting it leaves an explicit multi-country list in place (legacy bounties that only mirror a country-only location keep deriving that country from location), an empty array is stored as an explicit empty list but never lifts a country-only or on-site country restriction, and on-site bounties accept only their location country. Live capture and required-link requirements can only be changed before applications are received. applicationDetails may be changed after applications are received; existing applications keep their original answers, while future applications use the latest fields. applicationDetails are application detail items for standard application bounties only; blank rows are ignored, uploads are capped at 3 fields, acknowledgments at 5 fields, and camera-only live_video at 1 required field. lifecycleMessages can define auto-message templates for acceptance, rejection, and submission review transitions. Admin-only ongoing bounty settings are intentionally not exposed through MCP. You can also pause/unpause a bounty (status 'paused' stops new applications and outreach, including on a fully staffed bounty; status 'open' resumes it, and the resumed status is derived from its seats: open, partially_filled, or assigned when every seat is still filled), close an unassigned bounty and return its unused funding (status 'closed'), increase seats via spotsAvailable, keep pending applicants on fill via keepApplicantsOnFill, and change the auto-reassign completion deadline via completionWindowHours (any whole number of hours from 1 to 720; null disables; only affects seats confirmed after the edit), and toggle automatic applicant review via autoAccept (applications received while off stay pending; toggling on affects future applications only; ignored on aiManaged bounties) or adjust its mic-quality cutoff via autoAcceptMinMicScore (1-5 \u2014 applications scoring below your configured cutoff are auto-rejected, at/above proceed; null resets to the 3.0 default, below which applications only stay pending). The exclude-previous-participants list can be changed after posting: pass excludedParticipantSourceBountyIds with the full new list of up to ${MAX_EXCLUDED_PARTICIPANT_SOURCE_BOUNTIES} ids of your OWN earlier bounties (an empty array clears it); the same source rules as create_bounty apply, and a bounty can never exclude itself. Use cancel_bounty for cancellation and refund handling. Work completion and payment are system-managed from escrow and payout evidence; status 'completed' and 'paid' cannot be set directly.`,
2428
2444
  input: UpdateBountyRequest
2429
2445
  };
2430
2446
  var cancelBountySpec = {
@@ -3175,7 +3191,12 @@ var common = {
3175
3191
  invalidTimeFormat: "Invalid time format. Use HH:mm (e.g., 09:00, 14:30)."
3176
3192
  };
3177
3193
  var bounties = {
3178
- dryRunPreview: "This is a preview \u2014 the bounty has NOT been created yet. preview.fundingTotal is the total funding requirement (including the platform fee) before any existing wallet balance is applied, so it may be higher than the hosted checkout charge. Show this estimate to the operator and ask if they'd like to edit anything before posting. To create the bounty, call create_bounty again with the same parameters and dryRun omitted or set to false.",
3194
+ dryRunPreview: "This is a preview \u2014 the bounty has NOT been created yet, no funds were reserved, and no checkout was started. preview.fundingTotal is the total funding requirement (including the platform fee) before any existing wallet balance is applied, so it may be higher than the hosted checkout charge. Show this estimate to the operator and ask if they'd like to edit anything before posting. To create the bounty, call create_bounty again with the same parameters and dryRun omitted or set to false.",
3195
+ dryRunServerValidated: "serverValidation=passed: the RentAHuman API validated and normalized these parameters with the same rules as a real create (country targeting, pricing, screening fields, content moderation). The AI content review runs only on the real create, so a live bounty can additionally land in pending_review.",
3196
+ dryRunServerUnavailable: "serverValidation=unavailable: the RentAHuman API could not be reached, so this preview was computed locally and has NOT been validated by the server (country targeting, pricing rules and screening fields are only checked there). Tell the operator the preview is unverified and retry the preview before creating.",
3197
+ dryRunMockPreview: "serverValidation=skipped: mock mode computes the preview locally without contacting the API.",
3198
+ retryWithSameKey: (idempotencyKey) => `The API did not confirm whether the bounty was created. Retry create_bounty with the same parameters and the same idempotencyKey ("${idempotencyKey}") after about 30 seconds: a replay returns the original bounty (including its deposit_url) instead of creating a duplicate, and a 409 idempotency_in_progress means the first attempt is still running \u2014 wait and retry again. Do not change the idempotencyKey.`,
3199
+ retryWithoutKey: "The API did not confirm whether the bounty was created, and this call had no idempotencyKey, so a plain retry could create a duplicate. First call list_bounties (mine=true) to check for a bounty with this title (an unfunded one is in pending_deposit status and is not publicly visible). If it is missing, retry with an idempotencyKey so further retries are safe.",
3179
3200
  createdMultiPerson: (spotsAvailable) => `Multi-person bounty created successfully. Looking for ${spotsAvailable} humans.`,
3180
3201
  createdSingle: "Bounty created successfully. Humans can now view and apply.",
3181
3202
  depositRequired: "DEPOSIT REQUIRED: The bounty has been created but is NOT visible yet. Tell the operator checkout_total (it includes a platform fee) before they pay. They must complete checkout at the deposit_url to make the bounty live. Share this URL with the human who owns this account.",
@@ -3300,6 +3321,12 @@ var MOCK_HIRING_STATUSES = /* @__PURE__ */ new Set([
3300
3321
  "pending_funding",
3301
3322
  "partially_filled"
3302
3323
  ]);
3324
+ var asRecord = (value) => {
3325
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3326
+ return {};
3327
+ }
3328
+ return Object.fromEntries(Object.entries(value));
3329
+ };
3303
3330
  var mockHiringSpotsRemaining = (bounty) => MOCK_HIRING_STATUSES.has(bounty.status) ? Math.max(0, (bounty.spotsAvailable || 1) - (bounty.spotsFilled || 0)) : 0;
3304
3331
  var handleCreateBounty = (args) => Effect9.gen(function* () {
3305
3332
  const params = yield* decode(CreateBountyRequest)(args);
@@ -3317,6 +3344,11 @@ var handleCreateBounty = (args) => Effect9.gen(function* () {
3317
3344
  const screeningFields = screeningFieldsResult.fields;
3318
3345
  const excludedParticipantSourceBountyIds = params.excludedParticipantSourceBountyIds?.length ? params.excludedParticipantSourceBountyIds : void 0;
3319
3346
  const exclusionFields = excludedParticipantSourceBountyIds ? { excludedParticipantSourceBountyIds } : {};
3347
+ const {
3348
+ dryRun: _dryRun,
3349
+ excludedParticipantSourceBountyIds: _rawExclusion,
3350
+ ...createParams
3351
+ } = params;
3320
3352
  if (params.dryRun) {
3321
3353
  const spotsAvailable = params.spotsAvailable || 1;
3322
3354
  const currency = params.currency || "USD";
@@ -3329,6 +3361,7 @@ var handleCreateBounty = (args) => Effect9.gen(function* () {
3329
3361
  liveCaptureRequirement: params.liveCaptureRequirement || null,
3330
3362
  category: params.category || "computer-gigs",
3331
3363
  location: params.location || { isRemoteAllowed: true },
3364
+ supportedCountries: params.supportedCountries ?? null,
3332
3365
  deadline: params.deadline || null,
3333
3366
  estimatedHours: params.estimatedHours,
3334
3367
  estimatedDurationUnit: params.estimatedDurationUnit || null,
@@ -3365,11 +3398,66 @@ var handleCreateBounty = (args) => Effect9.gen(function* () {
3365
3398
  spotsAvailable
3366
3399
  })
3367
3400
  };
3401
+ if (config.mockMode) {
3402
+ return ok({
3403
+ success: true,
3404
+ dryRun: true,
3405
+ serverValidation: "skipped",
3406
+ preview,
3407
+ message: `${messages.bounties.dryRunPreview} ${messages.bounties.dryRunMockPreview}`
3408
+ });
3409
+ }
3410
+ const verification2 = yield* identity2.createVerification("create_bounty");
3411
+ const { idempotencyKey: _previewKey, ...previewParams } = createParams;
3412
+ const outcome = yield* api.post("/bounties", {
3413
+ ...previewParams,
3414
+ ...exclusionFields,
3415
+ applicationDetails: screeningFields,
3416
+ dryRun: true,
3417
+ ...verification2
3418
+ }).pipe(
3419
+ Effect9.map((result2) => ({
3420
+ kind: "server",
3421
+ result: asRecord(result2)
3422
+ })),
3423
+ Effect9.catchTag(
3424
+ "TimeoutError",
3425
+ () => Effect9.succeed({ kind: "unreachable" })
3426
+ ),
3427
+ Effect9.catchTag(
3428
+ "ApiError",
3429
+ (error) => Effect9.succeed(
3430
+ error.statusCode === 0 ? { kind: "unreachable" } : { kind: "rejected", error }
3431
+ )
3432
+ )
3433
+ );
3434
+ if (outcome.kind === "rejected") {
3435
+ return err({
3436
+ success: false,
3437
+ dryRun: true,
3438
+ error: outcome.error.body,
3439
+ errorType: "ApiError",
3440
+ statusCode: outcome.error.statusCode,
3441
+ endpoint: outcome.error.endpoint
3442
+ });
3443
+ }
3444
+ if (outcome.kind === "unreachable") {
3445
+ return ok({
3446
+ success: true,
3447
+ dryRun: true,
3448
+ serverValidation: "unavailable",
3449
+ preview,
3450
+ message: `${messages.bounties.dryRunPreview} ${messages.bounties.dryRunServerUnavailable}`
3451
+ });
3452
+ }
3453
+ const serverPreview = asRecord(outcome.result.preview);
3454
+ const fundingTotal = typeof outcome.result.fundingTotal === "number" ? outcome.result.fundingTotal : preview.fundingTotal;
3368
3455
  return ok({
3369
3456
  success: true,
3370
3457
  dryRun: true,
3371
- preview,
3372
- message: messages.bounties.dryRunPreview
3458
+ serverValidation: "passed",
3459
+ preview: { ...serverPreview, fundingTotal },
3460
+ message: `${messages.bounties.dryRunPreview} ${messages.bounties.dryRunServerValidated}`
3373
3461
  });
3374
3462
  }
3375
3463
  yield* rateLimit.check("create_bounty");
@@ -3387,6 +3475,7 @@ var handleCreateBounty = (args) => Effect9.gen(function* () {
3387
3475
  skillsNeeded: params.skillsNeeded || [],
3388
3476
  category: params.category || "computer-gigs",
3389
3477
  location: params.location || { isRemoteAllowed: true },
3478
+ ...params.supportedCountries !== void 0 ? { supportedCountries: params.supportedCountries } : {},
3390
3479
  deadline: params.deadline,
3391
3480
  estimatedHours: params.estimatedHours,
3392
3481
  priceType: params.priceType,
@@ -3428,18 +3517,42 @@ var handleCreateBounty = (args) => Effect9.gen(function* () {
3428
3517
  })
3429
3518
  );
3430
3519
  }
3431
- const {
3432
- dryRun: _,
3433
- excludedParticipantSourceBountyIds: _rawExclusion,
3434
- ...createParams
3435
- } = params;
3436
3520
  const verification = yield* identity2.createVerification("create_bounty");
3437
- const result = yield* api.post("/bounties", {
3521
+ const created = yield* api.post("/bounties", {
3438
3522
  ...createParams,
3439
3523
  ...exclusionFields,
3440
3524
  applicationDetails: screeningFields,
3441
3525
  ...verification
3442
- });
3526
+ }).pipe(
3527
+ Effect9.map((result2) => ({
3528
+ ok: true,
3529
+ result: asRecord(result2)
3530
+ })),
3531
+ // Upstream failures (gateway errors, timeouts, no response) leave the
3532
+ // agent unsure whether the bounty exists. Client errors keep their
3533
+ // normal error path — they are answers, not uncertainty.
3534
+ Effect9.catchTag(
3535
+ "TimeoutError",
3536
+ (error) => Effect9.succeed({ ok: false, error })
3537
+ ),
3538
+ Effect9.catchTag(
3539
+ "ApiError",
3540
+ (error) => error.statusCode === 0 || error.statusCode >= 500 ? Effect9.succeed({ ok: false, error }) : Effect9.fail(error)
3541
+ )
3542
+ );
3543
+ if (!created.ok) {
3544
+ const failure = created.error;
3545
+ const isApiError = "statusCode" in failure;
3546
+ return err({
3547
+ success: false,
3548
+ error: failure.message,
3549
+ errorType: isApiError ? "ApiError" : "TimeoutError",
3550
+ ...isApiError ? { statusCode: failure.statusCode } : {},
3551
+ endpoint: failure.endpoint,
3552
+ retry_guidance: params.idempotencyKey ? messages.bounties.retryWithSameKey(params.idempotencyKey) : messages.bounties.retryWithoutKey
3553
+ });
3554
+ }
3555
+ const result = created.result;
3443
3556
  if (result.deposit_url) {
3444
3557
  return ok({
3445
3558
  ...result,
@@ -3908,7 +4021,7 @@ var payEnterpriseBountyTool = toMcpTool(
3908
4021
  var bountyTools = [
3909
4022
  {
3910
4023
  name: "create_bounty",
3911
- description: `Create a one-shot task bounty for humans to apply to. To target an entire country, pass location with an ISO country code and isRemoteAllowed=false while omitting city and state. **IMPORTANT: Always call with dryRun=true first** to preview the bounty. Dry-run \`preview.fundingTotal\` is the total funding requirement before any existing wallet balance is applied. Show that estimate to the operator before posting. Show the preview to the user and ask 'Here's your bounty \u2014 would you like to edit anything before posting?' Only call again with dryRun=false (or omitted) after the user confirms. **You MUST help the user define completionCriteria and evidenceTypes** \u2014 ask what 'done' looks like and what proof they need (text/data, photos, video, or links). For specialized standard-application tasks, configure applicationDetails so applicants provide application details before review. Blank rows are ignored; application upload fields are one file each and capped at 3 total; acknowledgment checkboxes are capped at 5 total and can be optional or required; and one required live_video field may contain a script applicants must record with the in-browser camera. For appearance-based bounties where the worker appears on camera or their presence is part of the deliverable (sign holding, UGC, sponsored posts, on-camera video, promo/GTM appearances), always include one required live_video field with a label asking the applicant to explain in two sentences why they are a good fit \u2014 this gates applications on a live self-recorded video so the poster sees each applicant before accepting. Do not ask for passwords, OTP/2FA codes, API keys, private keys, seed phrases, government IDs, bank/card details, exact home addresses, dates of birth, or other sensitive personal information. For direct-review collection bounties, use submissionMode='photo_upload', 'video_upload', or 'document_upload' with matching submission settings instead of applicationDetails. lifecycleMessages can define auto-message templates for acceptance, rejection, and submission review transitions. You can require applicants to provide specific links (LinkedIn, GitHub, resume, etc.) using the requiredLinks parameter. Requires RENTAHUMAN_API_KEY from the account owner. Standard accounts use available wallet balance first; if the wallet cannot cover the bounty, the response includes deposit_url and checkout_total (the hosted charge after wallet balance is applied) and the account owner must complete checkout before the bounty is visible. Supports multi-person bounties by setting spotsAvailable > 1. Set completionWindowHours (any whole number of hours from 1 to 720, e.g. 36) to give confirmed workers a completion deadline: overdue seats auto-release and reopen for other applicants; workers may request extensions you answer with decide_extension_request. Pass optional \`idempotencyKey\` to make this safe to retry (a replayed key returns the original result instead of duplicating). Applicants are auto-reviewed by default (autoAccept, default true): deterministic checks always run and an AI review runs only for free-text screening answers \u2014 qualified applicants are accepted, clear mismatches rejected with a reason, uncertain cases left pending for manual review; pass autoAccept: false to review and accept every application yourself; on micCheckRequired bounties, set autoAcceptMinMicScore (1-5) to make the mic score a hard cutoff: applications below it are auto-rejected, at/above proceed toward acceptance, and unscored recordings stay pending. When omitted, the platform default 3.0 applies and lower scores only stay pending for manual review. To keep previous participants out, pass excludedParticipantSourceBountyIds with up to ${MAX_EXCLUDED_PARTICIPANT_SOURCE_BOUNTIES} ids of your OWN earlier bounties: any worker who was ever accepted on one of them cannot see or apply to the new bounty, and you can change the list later with update_bounty. AI-managed, QA, and taste run bounties, and never-funded drafts are rejected as sources. BETA: pass aiManaged: true to have the platform fully manage the bounty \u2014 automated recruiting, vetting, submission review, payment release, and a final report. Fixed price USD only, 1-50 spots. Poll the bounty's managed run or subscribe to run.report_ready for the finished work.`,
4024
+ description: `Create a one-shot task bounty for humans to apply to. To target an entire country, pass location with an ISO country code and isRemoteAllowed=false while omitting city and state. **IMPORTANT: Always call with dryRun=true first** to preview the bounty. A preview never creates a bounty, reserves funds, or starts a checkout. When the API is reachable it validates and normalizes the parameters with the same rules as a real create and the response carries serverValidation=passed; if the API cannot be reached the preview is computed locally and carries serverValidation=unavailable \u2014 tell the operator it is unverified. Dry-run \`preview.fundingTotal\` is the total funding requirement before any existing wallet balance is applied. Show that estimate to the operator before posting. Show the preview to the user and ask 'Here's your bounty \u2014 would you like to edit anything before posting?' Only call again with dryRun=false (or omitted) after the user confirms. **You MUST help the user define completionCriteria and evidenceTypes** \u2014 ask what 'done' looks like and what proof they need (text/data, photos, video, or links). For specialized standard-application tasks, configure applicationDetails so applicants provide application details before review. Blank rows are ignored; application upload fields are one file each and capped at 3 total; acknowledgment checkboxes are capped at 5 total and can be optional or required; and one required live_video field may contain a script applicants must record with the in-browser camera. For appearance-based bounties where the worker appears on camera or their presence is part of the deliverable (sign holding, UGC, sponsored posts, on-camera video, promo/GTM appearances), always include one required live_video field with a label asking the applicant to explain in two sentences why they are a good fit \u2014 this gates applications on a live self-recorded video so the poster sees each applicant before accepting. Do not ask for passwords, OTP/2FA codes, API keys, private keys, seed phrases, government IDs, bank/card details, exact home addresses, dates of birth, or other sensitive personal information. For direct-review collection bounties, use submissionMode='photo_upload', 'video_upload', or 'document_upload' with matching submission settings instead of applicationDetails. lifecycleMessages can define auto-message templates for acceptance, rejection, and submission review transitions. You can require applicants to provide specific links (LinkedIn, GitHub, resume, etc.) using the requiredLinks parameter. Requires RENTAHUMAN_API_KEY from the account owner. Standard accounts use available wallet balance first; if the wallet cannot cover the bounty, the response includes deposit_url and checkout_total (the hosted charge after wallet balance is applied) and the account owner must complete checkout before the bounty is visible. Supports multi-person bounties by setting spotsAvailable > 1. Set completionWindowHours (any whole number of hours from 1 to 720, e.g. 36) to give confirmed workers a completion deadline: overdue seats auto-release and reopen for other applicants; workers may request extensions you answer with decide_extension_request. Pass optional \`idempotencyKey\` to make this safe to retry (a replayed key returns the original result instead of duplicating). Applicants are auto-reviewed by default (autoAccept, default true): deterministic checks always run and an AI review runs only for free-text screening answers \u2014 qualified applicants are accepted, clear mismatches rejected with a reason, uncertain cases left pending for manual review; pass autoAccept: false to review and accept every application yourself; on micCheckRequired bounties, set autoAcceptMinMicScore (1-5) to make the mic score a hard cutoff: applications below it are auto-rejected, at/above proceed toward acceptance, and unscored recordings stay pending. When omitted, the platform default 3.0 applies and lower scores only stay pending for manual review. To keep previous participants out, pass excludedParticipantSourceBountyIds with up to ${MAX_EXCLUDED_PARTICIPANT_SOURCE_BOUNTIES} ids of your OWN earlier bounties: any worker who was ever accepted on one of them cannot see or apply to the new bounty, and you can change the list later with update_bounty. AI-managed, QA, and taste run bounties, and never-funded drafts are rejected as sources. BETA: pass aiManaged: true to have the platform fully manage the bounty \u2014 automated recruiting, vetting, submission review, payment release, and a final report. Fixed price USD only, 1-50 spots. Poll the bounty's managed run or subscribe to run.report_ready for the finished work.`,
3912
4025
  inputSchema: toInputSchema(CreateBountyRequest),
3913
4026
  handler: handleCreateBounty
3914
4027
  },
@@ -49,7 +49,7 @@ declare const CreateBountyRequest: Schema.Struct<{
49
49
  title: Schema.refine<string, Schema.filter<typeof Schema.String>>;
50
50
  description: Schema.refine<string, Schema.filter<typeof Schema.String>>;
51
51
  completionCriteria: Schema.refine<string, Schema.filter<typeof Schema.String>>;
52
- evidenceTypes: Schema.refine<readonly ("link" | "text" | "photo" | "video")[], Schema.Array$<Schema.Literal<["text", "photo", "video", "link"]>>>;
52
+ evidenceTypes: Schema.refine<readonly ("text" | "link" | "photo" | "video")[], Schema.Array$<Schema.Literal<["text", "photo", "video", "link"]>>>;
53
53
  evidenceCriteria: Schema.optional<Schema.filter<typeof Schema.String>>;
54
54
  liveCaptureRequirement: Schema.optional<Schema.Literal<["photo", "video"]>>;
55
55
  requirements: Schema.optional<Schema.Array$<typeof Schema.String>>;
@@ -61,6 +61,7 @@ declare const CreateBountyRequest: Schema.Struct<{
61
61
  country: Schema.optional<Schema.filter<typeof Schema.String>>;
62
62
  isRemoteAllowed: Schema.optional<typeof Schema.Boolean>;
63
63
  }>>;
64
+ supportedCountries: Schema.optional<Schema.filter<Schema.Array$<Schema.filter<Schema.filter<typeof Schema.String>>>>>;
64
65
  deadline: Schema.optional<typeof Schema.String>;
65
66
  estimatedHours: Schema.refine<number, Schema.filter<typeof Schema.Number>>;
66
67
  priceType: Schema.Literal<["fixed", "hourly"]>;
@@ -145,6 +146,7 @@ declare const UpdateBountyRequest: Schema.Struct<{
145
146
  country: Schema.optional<Schema.filter<typeof Schema.String>>;
146
147
  isRemoteAllowed: Schema.optional<typeof Schema.Boolean>;
147
148
  }>>;
149
+ supportedCountries: Schema.optional<Schema.filter<Schema.Array$<Schema.filter<Schema.filter<typeof Schema.String>>>>>;
148
150
  status: Schema.optional<Schema.Literal<["open", "in_review", "paused", "closed"]>>;
149
151
  identityRequired: Schema.optional<typeof Schema.Boolean>;
150
152
  excludedParticipantSourceBountyIds: Schema.optional<Schema.filter<Schema.Array$<typeof Schema.String>>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rentahuman-mcp",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "MCP server for AI agents to browse and book humans on rentahuman.ai",
5
5
  "keywords": [
6
6
  "ai",