Skip to content

feat(api): add idempotent pledge endpoint - #717

Merged
ritik4ever merged 2 commits into
ritik4ever:mainfrom
David-Adegboyega:feat/560-idempotent-pledge-endpoint
Jul 31, 2026
Merged

feat(api): add idempotent pledge endpoint#717
ritik4ever merged 2 commits into
ritik4ever:mainfrom
David-Adegboyega:feat/560-idempotent-pledge-endpoint

Conversation

@David-Adegboyega

@David-Adegboyega David-Adegboyega commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Pull Request

What Changed

A clear and concise description of what this PR changes and why.

Related Issues

Testing Done

Describe the tests you ran and how to reproduce them.

Security Review

If this PR touches API endpoints, authentication, database queries, or contract code, check the applicable items from SECURITY_CHECKLIST.md. Paste relevant items below:

- [ ] A01-1: Public endpoints correctly excluded from auth
- [ ] A03-1: SQL uses parameterised statements
- [ ] A03-2: User input validated with Zod schema
- [ ] A05-1: CORS scoped to known origins
- [ ] A09-1: API requests logged with request ID
- [ ] Dependency changes reviewed for vulnerabilities

Checklist

  • Code follows the existing style and patterns of the project
  • Tests added or updated to cover the change
  • All tests pass locally (npm test / cargo test)
  • UI changes include screenshots or a screen recording
  • PR title is descriptive and references the issue number
  • Security checklist items reviewed (if applicable)

Screenshots (if applicable)

Add screenshots or recordings for visual changes.

Summary by CodeRabbit

  • New Features

    • Added idempotent pledge creation using the Idempotency-Key header.
    • Duplicate requests return the original response without creating duplicate pledges.
    • Added X-Idempotency-Cache response headers indicating cache hits or misses.
    • Added Redis-backed caching with an in-memory fallback for development environments.
  • Bug Fixes

    • Prevented duplicate campaign claims and preserved claim history correctly.
  • Documentation

    • Expanded setup, database seeding, FAQ, troubleshooting, and pledge API guidance.

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

@David-Adegboyega is attempting to deploy a commit to the ritik4ever's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

@David-Adegboyega Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Idempotency-Key caching for pledge creation using Redis with an in-memory fallback, route middleware integration, API tests, OpenAPI and README documentation. The diff also contains an unresolved middleware import and duplicate catch syntax in cache initialization.

Changes

Pledge idempotency

Layer / File(s) Summary
Idempotency cache foundation
backend/src/services/idempotencyCache.ts, backend/src/services/cache.ts
Adds deterministic cache keys, Redis/LRU storage, TTL handling, and cache clearing; cache.ts contains duplicate catch syntax.
Pledge route middleware
backend/src/middleware/idempotencyMiddleware.ts, backend/src/index.ts
Looks up and replays cached responses, caches successful misses, and adds HIT/MISS headers; the route lacks the middleware import.
API contract and test coverage
backend/src/api.test.ts, backend/src/openapi.ts, CHANGELOG.md, README.md
Tests duplicate-request behavior and documents idempotency headers, cache expiry, setup, and troubleshooting guidance.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant idempotencyMiddleware
  participant PledgeHandler
  participant RedisCache
  Client->>idempotencyMiddleware: POST pledge with Idempotency-Key
  idempotencyMiddleware->>RedisCache: Read cached response
  RedisCache-->>idempotencyMiddleware: Hit or miss
  idempotencyMiddleware->>PledgeHandler: Continue on miss
  PledgeHandler-->>idempotencyMiddleware: Successful pledge response
  idempotencyMiddleware->>RedisCache: Cache response
  idempotencyMiddleware-->>Client: Return response with HIT or MISS header
Loading

Possibly related PRs

Suggested reviewers: teefeh-07, alphatechini

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The implementation targets [#560], but missing imports, invalid catch syntax, and no clear 24-hour expiration prevent the acceptance criteria from being met. Fix compilation errors, restore valid cache initialization, and implement and verify a 24-hour TTL for idempotency entries.
Out of Scope Changes check ⚠️ Warning The PR includes unrelated archive/restore, campaign-claim, deadline, README, and changelog changes beyond [#560]. Remove unrelated archive/restore, campaign-claim, deadline, and documentation changes or split them into separate pull requests.
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes adding idempotent pledge API behavior.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.5)
backend/src/services/cache.ts

File contains syntax errors that prevent linting: Line 39: Expected a statement but instead found 'catch'.; Line 42: Expected a statement but instead found 'catch'.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

backend/src/services/cache.ts

Parsing error: 'try' expected.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/api.test.ts`:
- Around line 487-491: Update all six idempotency test call sites to use the
existing postWithHeaders helper instead of post, passing each Idempotency-Key
through the headers argument. Keep unkeyed requests on post and ensure the tests
exercise the keyed idempotency path without changing their assertions.

In `@backend/src/middleware/idempotencyMiddleware.ts`:
- Around line 24-26: Update the idempotency cache-key construction in the
middleware around buildIdempotencyCacheKey to include the validated contributor
identity alongside apiKey and campaignId. Ensure contributor identity is
available from the validated request context, including for anonymous requests,
so identical idempotency keys cannot reuse another contributor’s pledge
response.

In `@backend/src/services/cache.ts`:
- Around line 38-40: Remove the redisClient = null and isConnected = false
assignments from the successful connection path after await
redisClient.connect(). Keep those state resets only in the failure/cleanup path
so successful initialization retains the connected client and connected status.

In `@backend/src/services/idempotencyCache.ts`:
- Around line 27-58: Replace the separate lookup/write flow around
getIdempotencyCacheEntry and setIdempotencyCacheEntry with an atomic claim
operation performed before the handler runs, using Redis’s conditional create
semantics (such as SET NX) and an equivalent memory-cache guard. When a key is
already claimed, wait for or replay the winning request’s stored response
instead of invoking next(); ensure the winning response is published through the
existing cache path for subsequent waiters.

In `@backend/src/validation/schemas.ts`:
- Line 5: Remove the unused CampaignStatus, CampaignSortField, and SortOrder
type imports from the schemas module while leaving all other imports unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 738db489-5446-4256-a9da-b9ef78c63100

📥 Commits

Reviewing files that changed from the base of the PR and between 10f827c and 0cdb613.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • README.md
  • backend/src/api.test.ts
  • backend/src/index.ts
  • backend/src/middleware/idempotencyMiddleware.ts
  • backend/src/openapi.ts
  • backend/src/services/cache.ts
  • backend/src/services/idempotencyCache.ts
  • backend/src/validation/schemas.ts

Comment thread backend/src/api.test.ts
Comment on lines +487 to +491
const res = await post(`/api/campaigns/${campaignId}/pledges`, {
contributor: CONTRIBUTOR_C,
amount: 100,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'test-key-1' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

These tests never send Idempotency-Keypost() takes only two arguments.

post(apiPath, body) (Lines 58-66) has no third parameter, so the { 'Idempotency-Key': ... } object is silently dropped at runtime and every one of these "idempotency" tests actually exercises the unkeyed path. This also fails tsc (TS2554: expected 2 arguments, got 3) if typecheck runs in CI. Concretely, Line 513 (secondRes.data deep-equals firstRes.data) and Line 535 (pledge count unchanged) cannot hold, since the second request creates a real second pledge.

All six call sites passing a third argument (Lines 487-491, 500-511, 519-532, 553-564, 575-587, 595-606) must use postWithHeaders instead.

🐛 Proposed fix (apply the same change to each keyed call site)
-    const res = await post(`/api/campaigns/${campaignId}/pledges`, {
-      contributor: CONTRIBUTOR_C,
-      amount: 100,
-      assetCode: 'USDC',
-    }, { 'Idempotency-Key': 'test-key-1' });
+    const res = await postWithHeaders(
+      `/api/campaigns/${campaignId}/pledges`,
+      { contributor: CONTRIBUTOR_C, amount: 100, assetCode: 'USDC' },
+      { 'Idempotency-Key': 'test-key-1' },
+    );

Alternatively, give post an optional headers parameter and drop postWithHeaders to avoid two near-identical helpers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const res = await post(`/api/campaigns/${campaignId}/pledges`, {
contributor: CONTRIBUTOR_C,
amount: 100,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'test-key-1' });
const res = await postWithHeaders(
`/api/campaigns/${campaignId}/pledges`,
{ contributor: CONTRIBUTOR_C, amount: 100, assetCode: 'USDC' },
{ 'Idempotency-Key': 'test-key-1' },
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/api.test.ts` around lines 487 - 491, Update all six idempotency
test call sites to use the existing postWithHeaders helper instead of post,
passing each Idempotency-Key through the headers argument. Keep unkeyed requests
on post and ensure the tests exercise the keyed idempotency path without
changing their assertions.

Comment on lines +24 to +26
const apiKey = (req as unknown as RequestWithApiKey).apiKey ?? 'anonymous';
const campaignId = req.params.id as string;
const cacheKey = buildIdempotencyCacheKey(apiKey, campaignId, idempotencyKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Scope the key to the contributor contract.

The cache key omits the contributor entirely. Requests from different contributors with the same API key—or any requests using the anonymous fallback—can replay another contributor’s pledge response instead of creating the intended pledge. Include the validated contributor identity in the cache scope (or reject reuse of a key with a different request fingerprint).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/middleware/idempotencyMiddleware.ts` around lines 24 - 26, Update
the idempotency cache-key construction in the middleware around
buildIdempotencyCacheKey to include the validated contributor identity alongside
apiKey and campaignId. Ensure contributor identity is available from the
validated request context, including for anonymous requests, so identical
idempotency keys cannot reuse another contributor’s pledge response.

Comment thread backend/src/services/cache.ts
Comment on lines +27 to +58
export async function getIdempotencyCacheEntry(
key: string,
): Promise<IdempotencyCacheEntry | null> {
if (isCacheAvailable()) {
const cached = await getCacheValue(key);
if (cached) {
return JSON.parse(cached) as IdempotencyCacheEntry;
}
}

const memoryEntry = memoryCache.get(key);
if (memoryEntry) {
return memoryEntry;
}

return null;
}

export async function setIdempotencyCacheEntry(
key: string,
entry: IdempotencyCacheEntry,
): Promise<void> {
const serialized = JSON.stringify(entry);

if (isCacheAvailable()) {
await setCacheValue(key, serialized, IDEMPOTENCY_TTL_SECONDS).catch(() => {
// Silently fail Redis writes; memory cache still works
});
}

memoryCache.set(key, entry);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Make idempotency acquisition atomic.

A lookup followed by a later write cannot prevent two concurrent requests with the same key from both missing and reaching the database. Add an atomic “claim/in-progress” operation (for Redis, e.g. SET ... NX) before invoking the handler; waiters must replay/wait for the winning response rather than call next().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/idempotencyCache.ts` around lines 27 - 58, Replace the
separate lookup/write flow around getIdempotencyCacheEntry and
setIdempotencyCacheEntry with an atomic claim operation performed before the
handler runs, using Redis’s conditional create semantics (such as SET NX) and an
equivalent memory-cache guard. When a key is already claimed, wait for or replay
the winning request’s stored response instead of invoking next(); ensure the
winning response is published through the existing cache path for subsequent
waiters.

Comment thread backend/src/validation/schemas.ts
@ritik4ever

Copy link
Copy Markdown
Owner

Hi @David-Adegboyega,

This PR could not be merged because it has merge conflicts with the target branch.

Please resolve the merge conflicts, push the updated changes, and the PR can be reviewed and merged.

Thank you!

@David-Adegboyega

Copy link
Copy Markdown
Contributor Author

@ritik4ever conflict resolved.

Comment thread backend/src/api.test.ts
contributor: CONTRIBUTOR_C,
amount: 100,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'test-key-1' });
Comment thread backend/src/api.test.ts
contributor: CONTRIBUTOR_C,
amount: 100,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'dup-key-1' });
Comment thread backend/src/api.test.ts
contributor: CONTRIBUTOR_C,
amount: 100,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'dup-key-1' });
Comment thread backend/src/api.test.ts
contributor: CONTRIBUTOR_C,
amount: 100,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'db-write-key' });
Comment thread backend/src/api.test.ts
contributor: CONTRIBUTOR_C,
amount: 100,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'db-write-key' });
Comment thread backend/src/api.test.ts
contributor: CONTRIBUTOR_C,
amount: 100,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'shared-key' });
Comment thread backend/src/api.test.ts
contributor: CONTRIBUTOR_D,
amount: 100,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'shared-key' });
Comment thread backend/src/api.test.ts
contributor: CONTRIBUTOR_C,
amount: 75,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'status-payload-key' });
Comment thread backend/src/api.test.ts
contributor: CONTRIBUTOR_C,
amount: 75,
assetCode: 'USDC',
}, { 'Idempotency-Key': 'status-payload-key' });
} catch {
redisClient = null;
isConnected = false;
} catch {

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
backend/src/services/cache.ts (1)

35-44: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicate handlers from the Redis initialization path.

backend/src/services/cache.ts#L35-L41 has one valid catch, then an extra bare catch at line 39 and another duplicate bare catch at line 42, which prevents the service from compiling. Keep one catch block that logs the Redis connection failure and resets Redis state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/cache.ts` around lines 35 - 44, Remove the duplicate
bare catch handlers from the Redis initialization path in
backend/src/services/cache.ts lines 35-44, keeping the single catch that logs
the failure and resets redisClient and isConnected. No direct changes are
required at backend/src/index.ts line 761 or backend/src/api.test.ts lines 472,
478, and 486; they are affected sites but are corrected by the cache handler
fix.

Source: Linters/SAST tools

backend/src/index.ts (2)

598-598: 🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Import idempotencyMiddleware and reserve idempotency keys before calling next.

idempotencyMiddleware is used in the pledge route but not imported, so this route will fail to compile. Even with the import, the middleware currently only reads the idempotency cache and re-writes it after res.send, allowing concurrent requests with the same key to both miss and create duplicate pledges. Reserve the key before next() with an atomic SET ... NX/wait-replay path and cover it with a parallel-request test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/index.ts` at line 598, Import idempotencyMiddleware in the pledge
route module, then update its implementation to atomically reserve each
idempotency key with SET NX before invoking next(); when reservation fails, wait
for and replay the existing result instead of proceeding. Add a parallel-request
test verifying that concurrent requests with the same key produce only one
pledge.

118-125: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expose the idempotency cache header through CORS.

idempotencyMiddleware sets X-Idempotency-Cache, but browser cross-origin clients cannot read non-safelisted CORS response headers unless this header is added to exposedHeaders.

Proposed fix
     exposedHeaders: [
       'X-Total-Count',
+      'X-Idempotency-Cache',
       'X-RateLimit-Limit',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/index.ts` around lines 118 - 125, Update the CORS configuration’s
exposedHeaders list to include X-Idempotency-Cache, alongside the existing
rate-limit and retry headers, so cross-origin clients can read the header set by
idempotencyMiddleware.
backend/src/openapi.ts (2)

431-434: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Do not hard-code a 64KB body limit in the API contract.

The server defaults to 16kb and permits MAX_BODY_SIZE overrides. Describe this as the configured limit, or derive the documented value from configuration.

Proposed fix
-  description: 'Request body exceeds the 64KB maximum limit',
+  description: 'Request body exceeds the configured maximum limit.',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/openapi.ts` around lines 431 - 434, The payloadTooLargeResponse
description hard-codes a 64KB limit that may not match the server configuration.
Update this response definition to describe the configured body limit, reusing
the existing MAX_BODY_SIZE or body-size configuration symbol, while preserving
the ApiError schema and response behavior.

636-648: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Declare Idempotency-Key as an optional request header.

POST /api/campaigns/{id}/pledges documents only the body in the generated OpenAPI. Add a request.headers schema with Idempotency-Key: z.string().optional() and a short description, e.g. “An optional idempotency key for this pledge request.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/openapi.ts` around lines 636 - 648, Update the POST pledge
endpoint definition near the body schema to add an optional request.headers
schema containing Idempotency-Key as an optional string with a short descriptive
annotation, while preserving the existing body and responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@backend/src/index.ts`:
- Line 598: Import idempotencyMiddleware in the pledge route module, then update
its implementation to atomically reserve each idempotency key with SET NX before
invoking next(); when reservation fails, wait for and replay the existing result
instead of proceeding. Add a parallel-request test verifying that concurrent
requests with the same key produce only one pledge.
- Around line 118-125: Update the CORS configuration’s exposedHeaders list to
include X-Idempotency-Cache, alongside the existing rate-limit and retry
headers, so cross-origin clients can read the header set by
idempotencyMiddleware.

In `@backend/src/openapi.ts`:
- Around line 431-434: The payloadTooLargeResponse description hard-codes a 64KB
limit that may not match the server configuration. Update this response
definition to describe the configured body limit, reusing the existing
MAX_BODY_SIZE or body-size configuration symbol, while preserving the ApiError
schema and response behavior.
- Around line 636-648: Update the POST pledge endpoint definition near the body
schema to add an optional request.headers schema containing Idempotency-Key as
an optional string with a short descriptive annotation, while preserving the
existing body and responses.

In `@backend/src/services/cache.ts`:
- Around line 35-44: Remove the duplicate bare catch handlers from the Redis
initialization path in backend/src/services/cache.ts lines 35-44, keeping the
single catch that logs the failure and resets redisClient and isConnected. No
direct changes are required at backend/src/index.ts line 761 or
backend/src/api.test.ts lines 472, 478, and 486; they are affected sites but are
corrected by the cache handler fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 88997596-d83b-4fdb-b51a-34c7fad0730c

📥 Commits

Reviewing files that changed from the base of the PR and between 0cdb613 and 1289393.

📒 Files selected for processing (5)
  • README.md
  • backend/src/api.test.ts
  • backend/src/index.ts
  • backend/src/openapi.ts
  • backend/src/services/cache.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

@ritik4ever
ritik4ever merged commit 110b71e into ritik4ever:main Jul 31, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add idempotent pledge endpoint with idempotency key

3 participants