Skip to content
Gauge

ax-check.com

Dwolla

dwolla.com

ax-check.com

Checked 29 September 2026

B

74/100

Agents need your login

Dwolla's quickstart works great, but pricing stays hidden.

Docs give a runnable sandbox quickstart with code in four languages, but the pricing page only says "Custom pricing" with no rates, units or limits.

4 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

Publish prices alongside the sales contact.

Pricing page states only 'Custom pricing' with no published rates; costs are gated behind a sales conversation.

Docs

State what each price includes and how overages are billed.

Pricing page gives no per-transaction units, rail-specific rates, or volume limits.

Docs

Return Markdown when clients request text/markdown.

Homepage returned text/html for a text/markdown request; no Markdown representation offered.

Recorded coding sessions

Same task, 3 models, no dwolla.com account. Recorded 29 September 2026 on Pi.

3 recorded · 3m 36s

3m 36s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelQwen 3.8 Maxcwd/sandbox/repo
Help me build a simple example using Dwolla. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

The user wants a simple example using Dwolla. Let me look at the repo first to see what's there.

Let me check the sandbox.

$ ls -la /sandbox/repo && cat /sandbox/repo/README* 2>/dev/null | head -50{"content":[{"text":"total 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 29 08:19 ..\ndrwxr-xr-x 8 root root 4096 Sep 24 20:05…ctrl+o
{"content":[{"text":"total 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 29 08:19 ..\ndrwxr-xr-x 8 root root 4096 Sep 24 20:05 .git\n-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore\n","type":"text"}]}
$ node -v; npm -v; python3 -V; env | grep -i dwolla{"content":[{"text":"v22.23.2\n10.9.8\nPython 3.12.3\n\n\nCommand exited with code 1","type":"text"}],"details":{}}
Thinking…

Empty repo. Dwolla is an ACH payments API (hosted product, REST API + SDKs). It has a sandbox environment. No credentials available though. Let me check network access and whether dwolla-v2 npm package is installable.

Dwolla pricing: 25¢ per transaction, $0 for... Actually Dwolla's pricing is publicly documented: pay-as-you-go, $0.25 per ACH transaction (for ACH Direct), no monthly fee, no setup fee. There was also a tiered/volume pricing. Let me be careful about stating pricing precisely — I should note it's approximate and they should confirm. Dwolla pricing page: "Pay as you go: 25¢ per transaction" historically. Also they mention no setup costs, no monthly fees. Let me verify with network access if possible.

Let me test network.

$ cd /tmp && timeout 30 npm view dwolla-v2 version 2>&1 | tail -3{"content":[{"text":"3.4.2\n","type":"text"}]}
$ timeout 20 curl -sS -o /dev/null -w "%{http_code}\n" https://api-sandbox.dwolla.com/ 2>&1; timeout 20 curl -sS https://www.dwolla.com/pricing 2>&1 | head -c 300{"content":[{"text":"401\n<!doctype html><html lang=\"en\"><head>\n <meta charset=\"utf-8\">\n <title>Payment API Pricing | Dwolla</title>\n <link rel=…ctrl+o
{"content":[{"text":"401\n<!doctype html><html lang=\"en\"><head>\n    <meta charset=\"utf-8\">\n    <title>Payment API Pricing | Dwolla</title>\n    <link rel=\"shortcut icon\" href=\"https://www.dwolla.com/hubfs/Dwolla%20logo.png\">\n    <meta name=\"description\" content=\"Custom pricing for ACH and real-time payment infrastructure. Vol","type":"text"}]}
Thinking…

Network works. npm registry works. api-sandbox.dwolla.com returns 401 (needs auth) — expected.

Let me fetch the pricing page text to get accurate info.

$ cd /tmp && timeout 30 curl -sS https://www.dwolla.com/pricing | python3 -c "{"content":[{"text":" Payment API Pricing | Dwolla Skip to content Close Platform Payment Methods Instant Payments Same Day ACH Standard ACH Features Digital Wa…ctrl+o
cd /tmp && timeout 30 curl -sS https://www.dwolla.com/pricing | python3 -c "
import sys,re,html
t=sys.stdin.read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S)
t=re.sub(r'<style.*?</style>','',t,flags=re.S)
t=re.sub(r'<[^>]+>',' ',t)
t=html.unescape(t)
t=re.sub(r'\s+',' ',t)
print(t[:4000])
"

{"content":[{"text":" Payment API Pricing | Dwolla Skip to content Close Platform Payment Methods Instant Payments Same Day ACH Standard ACH Features Digital Wallet Dwolla Dashboard Mass Pay Drop-In Components Webhooks Correlation ID Addenda Records Security Open Banking Services Tokenization Bank Verification Secure Exchange Solution Benefits Sandbox Environment Dedicated Support Solutions Use Cases B2B Payments Load/Unload Digital Wallet Payouts Industries Insurance Lending Real Estate Manufacturing Healthcare Developers Developer Docs Developer Forum Platform Support Learn Blog Resources Platform Support Pricing SEARCH Login Talk to Sales Platform Payment Methods Instant Payments Same Day ACH Standard ACH Features Digital Wallet Dwolla Dashboard Mass Pay Drop-In Components Webhooks Correlation ID Addenda Records Security Open Banking Services Tokenization Bank Verification Secure Exchange Solution Benefits Sandbox Environment Dedicated Support Solutions Use Cases B2B Payments Load/Unload Digital Wallet Payouts Industries Insurance Lending Real Estate Manufacturing Healthcare Developers Developer Docs Developer Forum Platform Support Learn Blog Resources Platform Support Pricing Login Talk to Sales PAY BY BANK Instant Payments Send funds instantly via the RTP® Network and FedNow® Service. Same Day ACH Move money faster with same-day bank transfers. Standard ACH Reliable, low-cost bank transfers at standard processing times. FEATURES Unified API One integration across ACH and real-time rails — less to build, less to maintain. Automated Payments Reduce manual workflows and exceptions with automated bank transfers. Mass Pay Disburse to thousands of recipients with a single API request. Open Banking Services Verify accounts instantly, check balances, and reduce fraud exposure. Digital Wallet Hold funds on-platform to initiate faster, more flexible transfers. Data Visibility Track, reconcile, and report on payment activity across all rails. Security Tokenization, bank verification, and 24/7 monitoring for high-stakes money movement. Integration Go live faster with robust developer docs and a dedicated integration team. Sandbox Environment Test payment flows and edge cases before you go live. Dedicated Support Expert support from onboarding through scale — real people when it matters. SOLUTIONS Enterprise Hold and move funds with an embedded digital wallet. Balance Hold and move funds with an embedded digital wallet. Connect Integrate Dwolla with your existing banking relationship. USE CASES B2B Payments Move large payment volumes with clean reconciliation and fewer exceptions. Marketplaces Pay out sellers and vendors reliably at scale. Property Management Automate rent, deposits, refunds, and owner payouts. Unload/Load Digital Wallet Move funds on and off your platform with a single integration. Payouts Pay out funds quickly and securely AP/AR Payments Reduce manual AP/AR workflows and reconciliation overhead. Buy Now, Pay Later (BNPL) Support installment disbursements and repayments with standardized exception handling. INDUSTRIES Insurance Automate reimbursements and disbursements with traceable, predictable outcomes. Real Estate Move high-stakes funds with the operational clarity and fraud controls real estate demands. Lending Standardize disbursements and repayments across ACH and real-time rails. Healthcare Modernize bank transfers for billing, claims, and provider payouts. Manufacturing Move supplier and vendor payments with less manual overhead and better visibility. Custom pricing built around how you move money. Dwolla is purpose-built for platforms and enterprises running high volumes of bank payments. Pricing is tailored to your transaction volume, rails, and integration needs — not a one-size-fits-all tier. Ready to talk through your use case? Tell us how you move money and we'll show you how Dwolla fits — and what it costs. Experience the Benefits of Dwolla’s Solution Single API Unify your payment operations experience through a single A\n","type":"text"}]}
Thinking…

Pricing is custom/quote-based now — no published per-transaction rate. Historically it was $0.25/transaction with no monthly fee. I should state plainly: Dwolla no longer publishes fixed pricing; it's custom/quote-based, negotiated by volume/rail. Sandbox is free. I can mention historical published pricing was 25¢/ACH transaction but that's no longer on the page — I should be careful and just say it's not published.

Now build the example. Plan: a small Node.js script using dwolla-v2 SDK that:

  1. Gets auth token (client_credentials)
  2. Creates a customer
  3. Adds a funding source (in sandbox, use verified bank accounts / or create bank account)
  4. Creates a transfer
  5. Handles errors

Since I have no credentials, I can't actually run it. But I can make it runnable when credentials are supplied, and verify the code compiles/loads (syntax check, dry-run mode that fails gracefully without credentials).

Keep it light: no local services. dwolla-v2 is a pure HTTP client — fine.

Let me build:

  • package.json
  • .env.example
  • src/dwollaClient.js
  • src/runExample.js — a flow: create customer → create verified funding source (sandbox) → create transfer → poll status
  • README.md with pricing + notes

Let me install dwolla-v2 and write files.

Actually, should I include dotenv? Keep deps minimal: dwolla-v2 only, use process.env, and a tiny .env loader manually? Node 22 supports --env-file flag. Use node --env-file=.env. That avoids dotenv dep. Good.

Let's check dwolla-v2 API surface quickly.

$ mkdir -p /tmp/dwtest && cd /tmp/dwtest && timeout 60 npm i dwolla-v2 --silent 2>&1 | tail -3 && node -e "const D=require('dwolla-v2'); console.log(Object.keys(D)); const c=new D.Client({environment:'s…{"content":[{"text":"[ 'Client' ]\n[ 'constructor' ]\n","type":"text"}]}ctrl+o
mkdir -p /tmp/dwtest && cd /tmp/dwtest && timeout 60 npm i dwolla-v2 --silent 2>&1 | tail -3 && node -e "const D=require('dwolla-v2'); console.log(Object.keys(D)); const c=new D.Client({environment:'sandbox',key:'k',secret:'s'}); console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(c)));"

Thinking…

Client has getToken() method presumably on prototype... let's check instance props.

$ cd /tmp/dwtest && node -e "{"content":[{"text":"[\n 'key', 'id',\n 'secret', 'environment',\n 'onGrant', 'authUrl',\n 'tokenUrl', 'apiUrl',\n 'auth', …ctrl+o
cd /tmp/dwtest && node -e "
const D=require('dwolla-v2');
const c=new D.Client({environment:'sandbox',key:'k',secret:'s'});
console.log(Object.keys(c));
console.log(typeof c.getToken);
" && ls node_modules/dwolla-v2/src/

{"content":[{"text":"[\n  'key',          'id',\n  'secret',       'environment',\n  'onGrant',      'authUrl',\n  'tokenUrl',     'apiUrl',\n  'auth',         'Auth',\n  'Token',        'get',\n  'post',         'delete',\n  'refreshToken', 'token'\n]\nundefined\ndwolla\nindex.d.ts\nindex.js\nutil\n","type":"text"}]}
Thinking…

No getToken in this version? Let's check index.d.ts for the API.

$ cd /tmp/dwtest && cat node_modules/dwolla-v2/src/index.d.ts | head -80 && grep -rn "getToken\|auth(" node_modules/dwolla-v2/src/index.js | head -20{"content":[{"text":"declare module \"dwolla-v2\" {\n export class Client {\n get: (\n path: string,\n query?: { [prop: string]: string },\n …ctrl+o
{"content":[{"text":"declare module \"dwolla-v2\" {\n  export class Client {\n    get: (\n      path: string,\n      query?: { [prop: string]: string },\n      headers?: { \"Idempotency-Key\"?: string } & { [prop: string]: string }\n    ) => Promise<Response>;\n    post: (\n      path: string,\n      body?: object,\n      headers?: { \"Idempotency-Key\"?: string } & { [prop: string]: string }\n    ) => Promise<Response>;\n    delete: (path: string) => Promise<Response>;\n    constructor(options: {\n      key: string;\n      secret: string;\n      environment?: \"production\" | \"sandbox\";\n    });\n  }\n\n  interface Response {\n    status: number;\n    headers: Headers;\n    body: any;\n  }\n}\n","type":"text"}]}
Thinking…

So dwolla-v2 v3 is a thin HTTP wrapper: client.get/post with automatic OAuth token handling (it manages tokens internally via auth). There's onGrant callback for token storage. Let me verify how auth works internally.

Let me look at index.js.

$ cd /tmp/dwtest && cat node_modules/dwolla-v2/src/index.js{"content":[{"text":"module.exports = {\n Client: require(\"./dwolla/Client\")\n};\n","type":"text"}]}
Thinking…

Let's read Client.js briefly to understand token handling & errors.

$ cd /tmp/dwtest && sed -n 1,120p node_modules/dwolla-v2/src/dwolla/Client.js; echo ---; sed -n 1,60p node_modules/dwolla-v2/src/dwolla/Token.js{"content":[{"text":"var invariant = require(\"invariant\");\nvar auth = require(\"./Auth\");\nvar Token = require(\"./Token\");\nvar isOneOfTypes = require(\".…ctrl+o
{"content":[{"text":"var invariant = require(\"invariant\");\nvar auth = require(\"./Auth\");\nvar Token = require(\"./Token\");\nvar isOneOfTypes = require(\"../util/isOneOfTypes\");\nvar snakifyKeys = require(\"../util/snakifyKeys\");\nvar TokenManager = require(\"./TokenManager\");\n\nvar DEFAULT_ENVIRONMENT = \"production\";\n\nvar ENVIRONMENTS = {\n  production: {\n    authUrl: \"https://accounts.dwolla.com/auth\",\n    tokenUrl: \"https://api.dwolla.com/token\",\n    apiUrl: \"https://api.dwolla.com\"\n  },\n  sandbox: {\n    authUrl: \"https://accounts-sandbox.dwolla.com/auth\",\n    tokenUrl: \"https://api-sandbox.dwolla.com/token\",\n    apiUrl: \"https://api-sandbox.dwolla.com\"\n  }\n};\n\nfunction Client(opts) {\n  invariant(typeof opts === \"object\", \"First argument must be an object.\");\n\n  this.id = this.key = opts.id || opts.key;\n  this.secret = opts.secret;\n  this.environment = opts.environment || DEFAULT_ENVIRONMENT;\n  this.onGrant = opts.onGrant;\n\n  invariant(\n    typeof opts.id === \"string\" || typeof opts.key === \"string\",\n    \"key is required.\"\n  );\n  invariant(typeof opts.secret === \"string\", \"secret is required.\");\n  invariant(this.environment in ENVIRONMENTS, \"Invalid environment.\");\n  invariant(\n    isOneOfTypes(opts.onGrant, [\"undefined\", \"function\"]),\n    \"Invalid onGrant.\"\n  );\n\n  this.authUrl = ENVIRONMENTS[this.environment].authUrl;\n  this.tokenUrl = ENVIRONMENTS[this.environment].tokenUrl;\n  this.apiUrl = ENVIRONMENTS[this.environment].apiUrl;\n\n  var self = this;\n\n  var thisAuth = auth(this);\n  this.auth = thisAuth.methods;\n  this.Auth = thisAuth.klass;\n\n  this.Token = Token.bind(null, this);\n\n  var getToken = TokenManager(this).getToken;\n\n  this.get = function() {\n    var getArgs = arguments;\n    return getToken().then(function(token) {\n      return token.get.apply(token, getArgs);\n    });\n  };\n\n  this.post = function() {\n    var postArgs = arguments;\n    return getToken().then(function(token) {\n      return token.post.apply(token, postArgs);\n    });\n  };\n\n  this.delete = function() {\n    var deleteArgs = arguments;\n    return getToken().then(function(token) {\n      return token.delete.apply(token, deleteArgs);\n    });\n  };\n\n  this.refreshToken = function(opts) {\n    snakifyKeys(opts);\n    return thisAuth.methods.refresh(new self.Token(opts));\n  };\n\n  this.token = function(opts) {\n    snakifyKeys(opts);\n    return new self.Token(opts);\n  };\n}\n\nif (process.env.NODE_ENV === \"test\") {\n  Client.ENVIRONMENTS = ENVIRONMENTS;\n}\n\nmodule.exports = Client;\n---\nvar fetch = require(\"node-fetch\").default;\nvar formurlencoded = require(\"form-urlencoded\").default;\nvar rejectEmptyKeys = require(\"../util/rejectEmptyKeys\");\nvar isFormData = require(\"../util/isFormData\");\nvar assign = Object.assign;\nvar Promise = require(\"bluebird\");\n\nfetch.Promise = Promise;\n\nvar Token = function(client, opts) {\n  this.client = client;\n  this.access_token = opts.access_token;\n  this.refresh_token = opts.refresh_token;\n  this.expires_in = opts.expires_in;\n  this.scope = opts.scope;\n  this.account_id = opts.account_id;\n};\n\nfunction getHeaders(token, moreHeaders) {\n  return assign(\n    {\n      Authorization: [\"Bearer\", token.access_token].join(\" \"),\n      Accept: \"application/vnd.dwolla.v1.hal+json\",\n      \"User-Agent\": require(\"./userAgent\")\n    },\n    moreHeaders\n  );\n}\n\nfunction getUrl(token, suppliedPath, suppliedQuery) {\n  var url;\n  if (typeof suppliedPath === \"object\") {\n    url = suppliedPath._links.self.href;\n  } else if (suppliedPath.startsWith(token.client.apiUrl)) {\n    url = suppliedPath;\n  } else if (suppliedPath.startsWith(\"/\")) {\n    url = [token.client.apiUrl, suppliedPath].join(\"\");\n  } else {\n    url = [\n      token.client.apiUrl,\n      suppliedPath.replace(/^https?:\\/\\/[^\\/]*\\//, \"\")\n    ].join(\"/\");\n  }\n  var query = formurlencoded(rejectEmptyKeys(suppliedQuery), {\n    skipIndex: true,\n    skipBracket: true\n  });\n  return query ? [url, query].join(\"?\") : url;\n}\n\nfunction errorFrom(message, parsedRes) {\n  var error = new Error(message);\n  error.status = parsedRes.status;\n  error.headers = parsedRes.headers;\n  error.body = parsedRes.body;\n  return error;\n}\n\nfunction handleResponse(res) {\n  return res.text().then(function(body) {\n","type":"text"}]}
Thinking…

Good. Now write the example project in /sandbox/repo.

Files:

  • package.json
  • .env.example
  • .gitignore (already exists; check contents)
  • src/client.js
  • src/example.js (create customer → funding source → transfer → status)
  • README.md

Also add a --check mode that validates wiring without credentials (no network). And maybe a small test with a mocked fetch? Keep light. I'll add a smoke test that stubs global fetch to assert the flow builds correct requests — that gives me something to actually run and verify without credentials. dwolla-v2 uses node-fetch internally (not global fetch), so stubbing global fetch won't work. Instead I can point the client at a local... no, no local services.

Alternative: use nock? That intercepts http module — node-fetch v2 uses http module, so nock works. But adding nock as a dev dep is fine and it's not a service stack. Let's do it: a mock test that asserts request shapes. That's a good verification without credentials.

Let me check .gitignore first and then write files.

$ cat /sandbox/repo/.gitignore{"content":[{"text":"# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript / TypeScript\nnode_modules/…ctrl+o
{"content":[{"text":"# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript / TypeScript\nnode_modules/\n.next/\ndist/\nbuild/\ncoverage/\n\n# Python\n.venv/\nvenv/\n__pycache__/\n*.py[cod]\n.pytest_cache/\n*.egg-info/\n\n# Rust\ntarget/\n\n# Go / PHP / Ruby vendored deps\nvendor/\n.bundle/\n\n# Java / Kotlin / Scala\n.gradle/\n\n# .NET\nbin/\nobj/\n\n# Elixir\n_build/\ndeps/\n\n# Dart / Flutter\n.dart_tool/\n","type":"text"}]}
Thinking…

Add .env to gitignore? It's not there. I'll append .env.

Now write files.

write /sandbox/repo/package.json{"content":[{"text":"Successfully wrote 419 bytes to /sandbox/repo/package.json","type":"text"}]}
write /sandbox/repo/.env.example{"content":[{"text":"Successfully wrote 193 bytes to /sandbox/repo/.env.example","type":"text"}]}
write /sandbox/repo/src/client.js{"content":[{"text":"Successfully wrote 1636 bytes to /sandbox/repo/src/client.js","type":"text"}]}
Thinking…

Now src/example.js: create a Customer (receive-only/unverified "Personal" customer), add a bank funding source, create transfer from master account to customer? Actually simplest flow:

  1. Create a customer (customerType personal, verified? For sandbox, unverified customers can receive transfers only... Actually transfers require both parties verified for most. Simplify: create a "Personal" customer, add bank funding source with verified: true in sandbox (sandbox lets you create verified bank accounts), then create a transfer from master account (root funding source) to the customer's funding source.

Endpoints:

  • POST /customers
  • POST /customers/{id}/funding-sources with {routingNumber, accountNumber, type, name, bankAccountType: "checking", verified: true} — in sandbox, verified: true works for creating a verified bank account.
  • GET /funding-sources (root funding sources for the master account)
  • POST /transfers with {_links: {source: {href}, destination: {href}}, amount: {currency:'USD', value:'10.00'}}
  • Idempotency-Key header

Also note sandbox "verified" funding source: POST /customers/{id}/funding-sources with {verified:true, routingNumber, accountNumber, type:'bank', name, bankAccountType}. In sandbox, that works; real env needs micro-deposits.

Include --dry-run that prints the plan without hitting network.

Also webhook note: transfers are async; poll GET /transfers/{id}.

Let me write example.js with clear steps, error handling for validation errors (body._embedded.errors).

write /sandbox/repo/src/example.js{"content":[{"text":"Successfully wrote 5152 bytes to /sandbox/repo/src/example.js","type":"text"}]}
Thinking…

Now a test with nock to verify request shapes without credentials. Install nock as devDependency.

But main() runs on require — in the test I'd require example.js which executes main(). That would attempt network. Let me guard: only run main if require.main === module. Fix that.

Then test: use nock to intercept api-sandbox.dwolla.com and accounts... Actually token request goes to https://api-sandbox.dwolla.com/token. Let's write the test.

edit /sandbox/repo/src/example.js{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/src/example.js.","type":"text"}],"details":{"diff":" ...\n 124 process.exitCode =…ctrl+o
{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/src/example.js.","type":"text"}],"details":{"diff":"     ...\n 124     process.exitCode = 1;\n 125   }\n 126 }\n 127 \n-128 main();\n+128 if (require.main === module) main();\n 129 \n 130 module.exports = { runFlow };","patch":"--- /sandbox/repo/src/example.js\n+++ /sandbox/repo/src/example.js\n@@ -124,7 +124,7 @@\n     process.exitCode = 1;\n   }\n }\n \n-main();\n+if (require.main === module) main();\n \n module.exports = { runFlow };\n","firstChangedLine":128}}
$ cd /sandbox/repo && timeout 90 npm i dwolla-v2 --silent && timeout 90 npm i -D nock --silent && node -e "console.log(require('nock/package.json').version)"{"content":[{"text":"14.0.17\n","type":"text"}]}
Thinking…

nock 14 — does it work with node-fetch v2 (uses http module)? node-fetch v2 uses http.request via http.request from 'http' module... node-fetch v2 uses require('http') and https directly, so nock should intercept. Let's write test and run.

write /sandbox/repo/test/example.test.js{"content":[{"text":"Successfully wrote 3055 bytes to /sandbox/repo/test/example.test.js","type":"text"}]}
Thinking…

Note: require('../src/example') will not execute main since require.main is test runner. Good.

Run tests and dry-run.

$ cd /sandbox/repo && timeout 90 npm test 2>&1 | tail -30{"content":[{"text":"# at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1045:22)\n# at Function._load (node:internal/modules/cjs/loader:1216:…ctrl+o
{"content":[{"text":"#     at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1045:22)\n#     at Function._load (node:internal/modules/cjs/loader:1216:25)\n#     at wrapModuleLoad (node:internal/modules/cjs/loader:254:19)\n#     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)\n#     at node:internal/main/run_main_module:36:49 {\n#   code: 'MODULE_NOT_FOUND',\n#   requireStack: []\n# }\n# Node.js v22.23.2\n# Subtest: test\nnot ok 1 - test\n  ---\n  duration_ms: 35.88674\n  type: 'test'\n  location: '/sandbox/repo/test:1:1'\n  failureType: 'testCodeFailure'\n  exitCode: 1\n  signal: ~\n  error: 'test failed'\n  code: 'ERR_TEST_FAILURE'\n  ...\n1..1\n# tests 1\n# suites 0\n# pass 0\n# fail 1\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 44.171556\n","type":"text"}]}
Thinking…

node --test test/ treats "test/" as a module? In Node 22, node --test test/ should run directory. The error MODULE_NOT_FOUND... maybe because it resolves "test/" as file. Use node --test test/*.test.js or node --test (auto-discovery). Let's change script to node --test.

edit /sandbox/repo/package.json{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/package.json.","type":"text"}],"details":{"diff":" ...\n 6 \"main\": \"src/example.…ctrl+o
{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/package.json.","type":"text"}],"details":{"diff":"    ...\n  6   \"main\": \"src/example.js\",\n  7   \"scripts\": {\n  8     \"example\": \"node --env-file=.env src/example.js\",\n  9     \"dry-run\": \"node src/example.js --dry-run\",\n-10     \"test\": \"node --test test/\"\n+10     \"test\": \"node --test\"\n 11   },\n 12   \"dependencies\": {\n 13     \"dwolla-v2\": \"^3.4.2\"\n 14   },\n    ...","patch":"--- /sandbox/repo/package.json\n+++ /sandbox/repo/package.json\n@@ -6,9 +6,9 @@\n   \"main\": \"src/example.js\",\n   \"scripts\": {\n     \"example\": \"node --env-file=.env src/example.js\",\n     \"dry-run\": \"node src/example.js --dry-run\",\n-    \"test\": \"node --test test/\"\n+    \"test\": \"node --test\"\n   },\n   \"dependencies\": {\n     \"dwolla-v2\": \"^3.4.2\"\n   },\n","firstChangedLine":10}}
Thinking…

Wait, nock 14 is ESM-only? nock v14 is ESM. Let's check — nock 14 requires ESM? I think nock v14 dropped CJS support. That will break require('nock'). Test will reveal. Let's run.

$ cd /sandbox/repo && timeout 90 npm test 2>&1 | tail -30{"content":[{"text":" code: 'ERR_TEST_FAILURE'\n name: 'TypeError'\n stack: |-\n performOnGrantCallback (/sandbox/repo/node_modules/dwolla-v2/src/dwolla/A…ctrl+o
{"content":[{"text":"  code: 'ERR_TEST_FAILURE'\n  name: 'TypeError'\n  stack: |-\n    performOnGrantCallback (/sandbox/repo/node_modules/dwolla-v2/src/dwolla/Auth.js:29:33)\n    tryCatcher (/sandbox/repo/node_modules/bluebird/js/release/util.js:16:23)\n    Promise._settlePromiseFromHandler (/sandbox/repo/node_modules/bluebird/js/release/promise.js:547:31)\n    Promise._settlePromise (/sandbox/repo/node_modules/bluebird/js/release/promise.js:604:18)\n    Promise._settlePromise0 (/sandbox/repo/node_modules/bluebird/js/release/promise.js:649:10)\n    Promise._settlePromises (/sandbox/repo/node_modules/bluebird/js/release/promise.js:729:18)\n    _drainQueueStep (/sandbox/repo/node_modules/bluebird/js/release/async.js:93:12)\n    _drainQueue (/sandbox/repo/node_modules/bluebird/js/release/async.js:86:9)\n    Async._drainQueues (/sandbox/repo/node_modules/bluebird/js/release/async.js:102:5)\n    Async.drainQueues (/sandbox/repo/node_modules/bluebird/js/release/async.js:15:14)\n    process.processImmediate (node:internal/timers:484:21)\n  ...\n# Subtest: createClient refuses to run without credentials\nok 2 - createClient refuses to run without credentials\n  ---\n  duration_ms: 1.65343\n  type: 'test'\n  ...\n1..2\n# tests 2\n# suites 0\n# pass 1\n# fail 1\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 243.865748\n","type":"text"}]}
Thinking…

nock loaded fine (CJS ok apparently, or node handled). The failure is in performOnGrantCallback — my onGrant callback receives a Token object and I access token.expires_in... it says TypeError at Auth.js:29. Let me look.

$ cd /sandbox/repo && sed -n 1,50p node_modules/dwolla-v2/src/dwolla/Auth.js; timeout 60 npm test 2>&1 | grep -m5 "message\|TypeError:"{"content":[{"text":"var fetch = require(\"node-fetch\").default;\nvar formurlencoded = require(\"form-urlencoded\").default;\nvar assign = Object.assign;\nvar …ctrl+o
{"content":[{"text":"var fetch = require(\"node-fetch\").default;\nvar formurlencoded = require(\"form-urlencoded\").default;\nvar assign = Object.assign;\nvar invariant = require(\"invariant\");\nvar rejectEmptyKeys = require(\"../util/rejectEmptyKeys\");\nvar toJson = require(\"../util/toJson\");\nvar snakifyKeys = require(\"../util/snakifyKeys\");\nvar Promise = require(\"bluebird\");\n\nfetch.Promise = Promise;\n\nfunction errorFrom(parsedRes) {\n  var error = new Error(JSON.stringify(parsedRes));\n  error.error = parsedRes.error;\n  error.error_description = parsedRes.error_description;\n  error.error_uri = parsedRes.error_uri;\n  return error;\n}\n\nfunction handleTokenResponse(client, res) {\n  if (res.error) {\n    return Promise.reject(errorFrom(res));\n  }\n  return new client.Token(res);\n}\n\nfunction performOnGrantCallback(client, token) {\n  if (client.onGrant) {\n    return client.onGrant(token).then(function() {\n      return token;\n    });\n  }\n  return token;\n}\n\nfunction requestToken(client, params) {\n  return fetch(client.tokenUrl, {\n    method: \"POST\",\n    compress: false,\n    headers: {\n      \"content-type\": \"application/x-www-form-urlencoded\",\n      \"user-agent\": require(\"../../src/dwolla/userAgent\")\n    },\n    body: formurlencoded(\n      assign(\n        {\n          client_id: client.id,\n          client_secret: client.secret\n        },\n        params\n\n\nCommand exited with code 1","type":"text"}],"details":{}}
Thinking…

onGrant must return a Promise. Fix client.js onGrant to return Promise.resolve().

edit /sandbox/repo/src/client.js{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/src/client.js.","type":"text"}],"details":{"diff":" ...\n 31 \n 32 const client = ne…ctrl+o
{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/src/client.js.","type":"text"}],"details":{"diff":"    ...\n 31 \n 32   const client = new Client({ key, secret, environment });\n 33 \n 34   // Optional: persist tokens between process runs. Sandbox tokens live ~1h.\n+35   // NOTE: onGrant must return a promise (the SDK chains on it). Returning\n+36   // undefined here breaks the first authenticated request.\n 35   client.onGrant = (token) => {\n 36     if (env.DWOLLA_DEBUG) {\n 37       console.error(`[dwolla] granted token, expires_in=${token.expires_in}s`);\n 38     }\n+41     return Promise.resolve(token);\n 39   };\n 40 \n 41   return client;\n 42 }\n    ...","patch":"--- /sandbox/repo/src/client.js\n+++ /sandbox/repo/src/client.js\n@@ -31,12 +31,15 @@\n \n   const client = new Client({ key, secret, environment });\n \n   // Optional: persist tokens between process runs. Sandbox tokens live ~1h.\n+  // NOTE: onGrant must return a promise (the SDK chains on it). Returning\n+  // undefined here breaks the first authenticated request.\n   client.onGrant = (token) => {\n     if (env.DWOLLA_DEBUG) {\n       console.error(`[dwolla] granted token, expires_in=${token.expires_in}s`);\n     }\n+    return Promise.resolve(token);\n   };\n \n   return client;\n }\n","firstChangedLine":35}}
Thinking…

Also DWOLLA_DEBUG isn't in the test env, but onGrant still returns promise — good. Re-run.

$ cd /sandbox/repo && timeout 90 npm test 2>&1 | tail -20 && echo "=== DRY RUN ===" && timeout 30 npm run dry-run --silent{"content":[{"text":" runFlow (/sandbox/repo/src/example.js:55:41)\n async TestContext.<anonymous> (/sandbox/repo/test/example.test.js:75:18)\n async T…ctrl+o
{"content":[{"text":"    runFlow (/sandbox/repo/src/example.js:55:41)\n    async TestContext.<anonymous> (/sandbox/repo/test/example.test.js:75:18)\n    async Test.run (node:internal/test_runner/test:1054:7)\n    async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)\n  ...\n# Subtest: createClient refuses to run without credentials\nok 2 - createClient refuses to run without credentials\n  ---\n  duration_ms: 3.252188\n  type: 'test'\n  ...\n1..2\n# tests 2\n# suites 0\n# pass 1\n# fail 1\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 240.770423\n=== DRY RUN ===\nDry run - no network calls. Planned sequence:\n\n  POST /customers                          create a Personal customer (the payee)\n  POST /customers/{id}/funding-sources     attach a bank account (sandbox: verified:true)\n  GET  /funding-sources                    find your master-account funding source (the payer)\n  POST /transfers                          move money, with an Idempotency-Key\n  GET  /transfers/{id}                     poll until it leaves 'pending'\n\nClient construction OK. Re-run as `npm run example` with real sandbox keys.\n","type":"text"}]}
Thinking…

Failure at example.js:55 — client.get(customerUrl)? line 55 is... let's see. Probably headers.get("location") returns undefined because nock lowercases header 'Location' → node http normalizes to lowercase, so headers.get('location') works... Actually node-fetch Headers.get is case-insensitive. Hmm. Let me see the actual error message.

$ cd /sandbox/repo && timeout 90 npm test 2>&1 | grep -A6 "not ok 1"{"content":[{"text":"not ok 1 - runFlow performs the documented payout sequence\n ---\n duration_ms: 55.166772\n type: 'test'\n location: '/sandbox/repo/tes…ctrl+o
{"content":[{"text":"not ok 1 - runFlow performs the documented payout sequence\n  ---\n  duration_ms: 55.166772\n  type: 'test'\n  location: '/sandbox/repo/test/example.test.js:12:1'\n  failureType: 'testCodeFailure'\n  error: 'Response has no \"self\" link: {\"status\":200,\"headers\":{},\"body\":{\"id\":\"cust-1\",\"_links\":{\"self\":{\"href\":\"https://api-sandbox.dwolla.com/customers/cust-1\"},\"funding-sources\":{\"href\":\"https://api-sandbox.dwolla.com/customers/cust-1/funding-sources\"}}}}'\n","type":"text"}]}
Thinking…

The SDK response shape is {status, headers, body}, so I must use res.body for HAL content and res.headers.get(...). Fix example.js: use customerRes.body._links, etc.

Wait — headers is a plain object {} here (nock/serialization). customerRes.headers.get("location") returned the right URL apparently (since GET /customers/cust-1 was intercepted). Good, headers.get works.

Fix all linkOf calls to use .body.

Lines:

  • linkOf(customer, "self") → linkOf(customer.body, "self")
  • linkOf(customer, "funding-sources") → customer.body
  • linkOf(source, "self") → source is from body._embedded already, fine
  • transfer.body.status — already correct.
edit /sandbox/repo/src/example.js{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/src/example.js.","type":"text"}],"details":{"diff":" ...\n 51 type: \"personal\"…ctrl+o
{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/src/example.js.","type":"text"}],"details":{"diff":"     ...\n  51     type: \"personal\",\n  52   });\n  53   const customerUrl = customerRes.headers.get(\"location\");\n  54   const customer = await client.get(customerUrl);\n- 55   console.log(`1. customer created -> ${linkOf(customer, \"self\")}`);\n+ 55   console.log(`1. customer created -> ${linkOf(customer.body, \"self\")}`);\n  56 \n  57   // 2. Attach a bank account. In sandbox `verified: true` is allowed; in\n  58   //    production you must verify via micro-deposits or an instant-verification\n  59   //    partner (e.g. Plaid) before the account can be used as a destination.\n- 60   const fsRes = await client.post(linkOf(customer, \"funding-sources\"), {\n+ 60   // SDK responses are { status, headers, body }; HAL content lives in .body.\n+ 61   const fsRes = await client.post(linkOf(customer.body, \"funding-sources\"), {\n  61     routingNumber: \"222222226\", // sandbox test routing number\n  62     accountNumber: \"123456789\",\n  63     type: \"bank\",\n  64     bankAccountType: \"checking\",\n     ...","patch":"--- /sandbox/repo/src/example.js\n+++ /sandbox/repo/src/example.js\n@@ -51,14 +51,15 @@\n     type: \"personal\",\n   });\n   const customerUrl = customerRes.headers.get(\"location\");\n   const customer = await client.get(customerUrl);\n-  console.log(`1. customer created -> ${linkOf(customer, \"self\")}`);\n+  console.log(`1. customer created -> ${linkOf(customer.body, \"self\")}`);\n \n   // 2. Attach a bank account. In sandbox `verified: true` is allowed; in\n   //    production you must verify via micro-deposits or an instant-verification\n   //    partner (e.g. Plaid) before the account can be used as a destination.\n-  const fsRes = await client.post(linkOf(customer, \"funding-sources\"), {\n+  // SDK responses are { status, headers, body }; HAL content lives in .body.\n+  const fsRes = await client.post(linkOf(customer.body, \"funding-sources\"), {\n     routingNumber: \"222222226\", // sandbox test routing number\n     accountNumber: \"123456789\",\n     type: \"bank\",\n     bankAccountType: \"checking\",\n","firstChangedLine":55}}
Thinking…

Also step 3: rootRes.body._embedded — correct already. Step 5 transfer.body.status — correct.

Re-run test.

$ cd /sandbox/repo && timeout 90 npm test 2>&1 | tail -25{"content":[{"text":" code: 'ERR_ASSERTION'\n name: 'AssertionError'\n expected: true\n actual: false\n operator: '=='\n stack: |-\n TestContext.<anony…ctrl+o
{"content":[{"text":"  code: 'ERR_ASSERTION'\n  name: 'AssertionError'\n  expected: true\n  actual: false\n  operator: '=='\n  stack: |-\n    TestContext.<anonymous> (/sandbox/repo/test/example.test.js:86:10)\n    async Test.run (node:internal/test_runner/test:1054:7)\n    async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)\n  ...\n# Subtest: createClient refuses to run without credentials\nok 2 - createClient refuses to run without credentials\n  ---\n  duration_ms: 1.563562\n  type: 'test'\n  ...\n1..2\n# tests 2\n# suites 0\n# pass 1\n# fail 1\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 1244.972913\n","type":"text"}]}
Thinking…

Line 86 is assert.ok(scope.isDone()). Duration 1244ms → polling happened (5 attempts... actually processed returned at attempt 2, so 1s delay). scope.isDone() false — probably one of the /transfers/xfer-1 GETs remained (I set .times(4) after first, so 5 total GETs but only 2 used → nock pending). Fix: use .persist() or only intercept enough. Better: use .times(5) won't help. Use persist and remove leftover assertion issues: with persist, isDone() returns true once interceptor used at least once.

Change: remove the separate first GET, use .get("/transfers/xfer-1").twice() ... Simplest: reply with processed immediately on first GET so no delay and no leftover. But then I don't test the polling loop. Compromise: first GET → pending, then .get(...).persist().reply(processed). Polling: attempt1 pending (1s sleep), attempt2 processed. Leftover persisted interceptor counts as done. Let's do that, and reduce sleep? Keep 1s, fine.

Also add nock.abortPendingRequests? Not needed. Also disable net connect in tests to prove no real network: nock.disableNetConnect(). Good — that also proves no local services used.

edit /sandbox/repo/test/example.test.js{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/test/example.test.js.","type":"text"}],"details":{"diff":" ...\n 8 \n 9 const API…ctrl+o
{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/test/example.test.js.","type":"text"}],"details":{"diff":"     ...\n   8 \n   9 const API = \"https://api-sandbox.dwolla.com\";\n  10 const HAL = { \"content-type\": \"application/vnd.dwolla.v1.hal+json\" };\n  11 \n+ 12 // Prove the flow is fully mocked: any real network access makes the test fail.\n+ 13 nock.disableNetConnect();\n+ 14 \n  12 test(\"runFlow performs the documented payout sequence\", async () => {\n  13   const seen = [];\n  14 \n  15   const scope = nock(API)\n     ...\n  60     })\n  61     .matchHeader(\"Idempotency-Key\", /^example-\\d+$/)\n  62     .reply(201, {}, { Location: `${API}/transfers/xfer-1` })\n  63     .get(\"/transfers/xfer-1\")\n- 64     .reply(200, { id: \"xfer-1\", status: \"pending\" }, HAL)\n+ 67     .reply(200, { id: \"xfer-1\", status: \"pending\" }, HAL) // first poll: still pending\n  65     .get(\"/transfers/xfer-1\")\n- 66     .times(4)\n+ 69     .persist()\n  67     .reply(200, { id: \"xfer-1\", status: \"processed\" }, HAL);\n  68 \n  69   const client = createClient({\n  70     DWOLLA_APP_KEY: \"k\",\n     ...","patch":"--- /sandbox/repo/test/example.test.js\n+++ /sandbox/repo/test/example.test.js\n@@ -8,8 +8,11 @@\n \n const API = \"https://api-sandbox.dwolla.com\";\n const HAL = { \"content-type\": \"application/vnd.dwolla.v1.hal+json\" };\n \n+// Prove the flow is fully mocked: any real network access makes the test fail.\n+nock.disableNetConnect();\n+\n test(\"runFlow performs the documented payout sequence\", async () => {\n   const seen = [];\n \n   const scope = nock(API)\n@@ -60,11 +63,11 @@\n     })\n     .matchHeader(\"Idempotency-Key\", /^example-\\d+$/)\n     .reply(201, {}, { Location: `${API}/transfers/xfer-1` })\n     .get(\"/transfers/xfer-1\")\n-    .reply(200, { id: \"xfer-1\", status: \"pending\" }, HAL)\n+    .reply(200, { id: \"xfer-1\", status: \"pending\" }, HAL) // first poll: still pending\n     .get(\"/transfers/xfer-1\")\n-    .times(4)\n+    .persist()\n     .reply(200, { id: \"xfer-1\", status: \"processed\" }, HAL);\n \n   const client = createClient({\n     DWOLLA_APP_KEY: \"k\",\n","firstChangedLine":12}}
Thinking…

But pending poll sleeps 1s — acceptable. Run test.

Also add teardown nock.cleanAll / enableNetConnect? Node test runner process exits. Fine, but add after hook for hygiene. Keep it simple.

$ cd /sandbox/repo && timeout 90 npm test 2>&1 | tail -25{"content":[{"text":" error: 'nock(...).post(...).reply(...).post(...).reply(...).get(...).reply(...).post(...).reply(...).get(...).reply(...).post(...).matchH…ctrl+o
{"content":[{"text":"  error: 'nock(...).post(...).reply(...).post(...).reply(...).get(...).reply(...).post(...).reply(...).get(...).reply(...).post(...).matchHeader(...).reply(...).get(...).reply(...).get(...).persist is not a function'\n  code: 'ERR_TEST_FAILURE'\n  name: 'TypeError'\n  stack: |-\n    TestContext.<anonymous> (/sandbox/repo/test/example.test.js:69:6)\n    Test.runInAsyncScope (node:async_hooks:214:14)\n    Test.run (node:internal/test_runner/test:1047:25)\n    Test.start (node:internal/test_runner/test:944:17)\n    startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)\n  ...\n# Subtest: createClient refuses to run without credentials\nok 2 - createClient refuses to run without credentials\n  ---\n  duration_ms: 0.839154\n  type: 'test'\n  ...\n1..2\n# tests 2\n# suites 0\n# pass 1\n# fail 1\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 184.419411\n","type":"text"}]}
Thinking…

persist() is on the Scope, not interceptor chain. So: call .persist() before the .get(). Restructure:

.get("/transfers/xfer-1").reply(200, pending)   // consumed once
.persist()  -> not allowed here

Instead build scope in two statements: const scope = nock(API)...chain ending with .reply(pending); scope.persist(); // hmm persist on scope applies to all interceptors? persist makes all interceptors persistent — that's fine for leftovers? Actually persist() makes each interceptor never be removed; then subsequent .get defined after persist... Let's just do: define the pending GET in the chain, then nock(API).persist().get("/transfers/xfer-1").reply(processed) as a separate scope, and assert only the first scope isDone plus that seen list is right. Interceptors are matched in order of definition, so pending matches first, then persist one. Good.

Also then scope.isDone() refers only to first scope (which excludes the second), so assertion works.

But the persisted second scope remains pending → isDone on first scope only. Fine.

edit /sandbox/repo/test/example.test.js{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/test/example.test.js.","type":"text"}],"details":{"diff":" ...\n 63 })\n 64 …ctrl+o
{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/test/example.test.js.","type":"text"}],"details":{"diff":"     ...\n  63     })\n  64     .matchHeader(\"Idempotency-Key\", /^example-\\d+$/)\n  65     .reply(201, {}, { Location: `${API}/transfers/xfer-1` })\n  66     .get(\"/transfers/xfer-1\")\n- 67     .reply(200, { id: \"xfer-1\", status: \"pending\" }, HAL) // first poll: still pending\n- 68     .get(\"/transfers/xfer-1\")\n+ 67     .reply(200, { id: \"xfer-1\", status: \"pending\" }, HAL); // first poll: still pending\n+ 68 \n+ 69   // Remaining polls settle. Defined on a second scope so the first scope's\n+ 70   // isDone() check still verifies the exact expected call sequence.\n+ 71   const settle = nock(API)\n  69     .persist()\n+ 73     .get(\"/transfers/xfer-1\")\n  70     .reply(200, { id: \"xfer-1\", status: \"processed\" }, HAL);\n  71 \n  72   const client = createClient({\n  73     DWOLLA_APP_KEY: \"k\",\n     ...\n  86       \"POST /transfers\",\n  87     ]\n  88   );\n  89   assert.ok(scope.isDone(), \"all expected HTTP calls were made\");\n+ 94   assert.ok(settle.isDone(), \"transfer was polled until it settled\");\n  90 });\n  91 \n  92 test(\"createClient refuses to run without credentials\", () => {\n  93   assert.throws(() => createClient({ DWOLLA_ENV: \"sandbox\" }), {\n     ...","patch":"--- /sandbox/repo/test/example.test.js\n+++ /sandbox/repo/test/example.test.js\n@@ -63,11 +63,15 @@\n     })\n     .matchHeader(\"Idempotency-Key\", /^example-\\d+$/)\n     .reply(201, {}, { Location: `${API}/transfers/xfer-1` })\n     .get(\"/transfers/xfer-1\")\n-    .reply(200, { id: \"xfer-1\", status: \"pending\" }, HAL) // first poll: still pending\n-    .get(\"/transfers/xfer-1\")\n+    .reply(200, { id: \"xfer-1\", status: \"pending\" }, HAL); // first poll: still pending\n+\n+  // Remaining polls settle. Defined on a second scope so the first scope's\n+  // isDone() check still verifies the exact expected call sequence.\n+  const settle = nock(API)\n     .persist()\n+    .get(\"/transfers/xfer-1\")\n     .reply(200, { id: \"xfer-1\", status: \"processed\" }, HAL);\n \n   const client = createClient({\n     DWOLLA_APP_KEY: \"k\",\n@@ -86,8 +90,9 @@\n       \"POST /transfers\",\n     ]\n   );\n   assert.ok(scope.isDone(), \"all expected HTTP calls were made\");\n+  assert.ok(settle.isDone(), \"transfer was polled until it settled\");\n });\n \n test(\"createClient refuses to run without credentials\", () => {\n   assert.throws(() => createClient({ DWOLLA_ENV: \"sandbox\" }), {\n","firstChangedLine":67}}
Thinking…

Also add nock.cleanAll(); nock.enableNetConnect(); after test? Add at end of file with test.after. Let's just add after the first test body: nock.cleanAll(). Actually cleanAll would remove persisted interceptors used by nothing else. Add at end of test: nock.cleanAll(). Then assert settle.isDone() before. Fine — add cleanAll as last line of test 1.

$ cd /sandbox/repo && python3 - <<'EOF'{"content":[{"text":"# 5. transfer status: processed (attempt 2)\n# Subtest: runFlow performs the documented payout sequence\nok 1 - runFlow performs the docume…ctrl+o
cd /sandbox/repo && python3 - <<'EOF'
import re
p='test/example.test.js'
s=open(p).read()
s=s.replace('  assert.ok(settle.isDone(), "transfer was polled until it settled");','  assert.ok(settle.isDone(), "transfer was polled until it settled");\n  nock.cleanAll();')
open(p,'w').write(s)
EOF
timeout 90 npm test 2>&1 | tail -22

{"content":[{"text":"# 5. transfer status: processed (attempt 2)\n# Subtest: runFlow performs the documented payout sequence\nok 1 - runFlow performs the documented payout sequence\n  ---\n  duration_ms: 1101.764105\n  type: 'test'\n  ...\n# Subtest: createClient refuses to run without credentials\nok 2 - createClient refuses to run without credentials\n  ---\n  duration_ms: 0.881804\n  type: 'test'\n  ...\n1..2\n# tests 2\n# suites 0\n# pass 2\n# fail 0\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 1311.372859\n","type":"text"}]}
Thinking…

Both tests pass with netConnect disabled — so no real network needed, and no local services.

Now also verify the real credential-less run behaves nicely: node src/example.js → exit code 2 with message. Test it.

Then write README.md with pricing + manageability assessment.

Also add .env to .gitignore.

Pricing research: Dwolla pricing page says custom pricing, no published rates. Let me quickly check docs for any fee info? Historically 25¢/transaction. I'll state: Dwolla does not publish pricing; it's quote-based/custom, sandbox free, no self-serve signup. I should double check there's no published pricing on docs. Quick grep of docs site maybe. Let me just do one quick check on docs for "pricing" mention of fees. Probably not necessary; keep concise and honest.

Let me run the no-cred check and check sandbox docs for verified funding source note? Keep light.

$ cd /sandbox/repo && timeout 30 node src/example.js; echo "exit=$?"; printf '\n.env\n' >> .gitignore; tail -3 .gitignore{"content":[{"text":"/sandbox/repo/src/client.js:20\n const err = new Error(\n ^\n\nError: Missing DWOLLA_APP_KEY / DWOLLA_APP_SECRET. Copy .e…ctrl+o
{"content":[{"text":"/sandbox/repo/src/client.js:20\n    const err = new Error(\n                ^\n\nError: Missing DWOLLA_APP_KEY / DWOLLA_APP_SECRET. Copy .env.example to .env and fill in sandbox credentials from https://dashboard-sandbox.dwolla.com/applications\n    at createClient (/sandbox/repo/src/client.js:20:17)\n    at main (/sandbox/repo/src/example.js:114:18)\n    at Object.<anonymous> (/sandbox/repo/src/example.js:129:30)\n    at Module._compile (node:internal/modules/cjs/loader:1781:14)\n    at Object..js (node:internal/modules/cjs/loader:1913:10)\n    at Module.load (node:internal/modules/cjs/loader:1505:32)\n    at Function._load (node:internal/modules/cjs/loader:1309:12)\n    at wrapModuleLoad (node:internal/modules/cjs/loader:254:19)\n    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)\n    at node:internal/main/run_main_module:36:49 {\n  code: 'DWOLLA_MISSING_CREDENTIALS'\n}\n\nNode.js v22.23.2\nexit=1\n.dart_tool/\n\n.env\n","type":"text"}]}
Thinking…

The throw happens outside try/catch (createClient is called before try). Move it inside try. Fix main().

edit /sandbox/repo/src/example.js{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/src/example.js.","type":"text"}],"details":{"diff":" ...\n 110 console.log(\"\\nC…ctrl+o
{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/src/example.js.","type":"text"}],"details":{"diff":"     ...\n 110     console.log(\"\\nClient construction OK. Re-run as `npm run example` with real sandbox keys.\");\n 111     return;\n 112   }\n 113 \n-114   const client = createClient();\n 115   try {\n+115     const client = createClient();\n 116     const result = await runFlow(client, { amount: process.env.DWOLLA_AMOUNT || \"10.00\" });\n 117     console.log(\"\\nDone:\", JSON.stringify(result.status));\n 118   } catch (err) {\n 119     if (err.code === \"DWOLLA_MISSING_CREDENTIALS\") {\n     ...","patch":"--- /sandbox/repo/src/example.js\n+++ /sandbox/repo/src/example.js\n@@ -110,10 +110,10 @@\n     console.log(\"\\nClient construction OK. Re-run as `npm run example` with real sandbox keys.\");\n     return;\n   }\n \n-  const client = createClient();\n   try {\n+    const client = createClient();\n     const result = await runFlow(client, { amount: process.env.DWOLLA_AMOUNT || \"10.00\" });\n     console.log(\"\\nDone:\", JSON.stringify(result.status));\n   } catch (err) {\n     if (err.code === \"DWOLLA_MISSING_CREDENTIALS\") {\n","firstChangedLine":114}}
Thinking…

I need to make sure the client declaration is inside the try block so it's accessible throughout the try/catch scope.

$ cd /sandbox/repo && timeout 30 node src/example.js; echo "exit=$?"; timeout 60 npm test 2>&1 | grep -E "^# (pass|fail)"{"content":[{"text":"\nMissing DWOLLA_APP_KEY / DWOLLA_APP_SECRET. Copy .env.example to .env and fill in sandbox credentials from https://dashboard-sandbox.dwol…ctrl+o
{"content":[{"text":"\nMissing DWOLLA_APP_KEY / DWOLLA_APP_SECRET. Copy .env.example to .env and fill in sandbox credentials from https://dashboard-sandbox.dwolla.com/applications\n\nexit=2\n# pass 2\n# fail 0\n","type":"text"}]}
Thinking…

Both work. Now README with:

  • what it does
  • setup/run
  • pricing (honest)
  • manageability assessment for me (the agent)
  • limitations/blocked: I couldn't run against real sandbox because no credentials; onboarding requires a business application/approval — that's a blocker for me acting end-to-end.

Let me double check pricing claim with one more source — maybe docs mention fees. Quick check: dwolla docs "pricing"? I'll do a fast grep for "$0.25" across dwolla.com pages? Not worth it; state it plainly as custom/quote-based, and note sandbox is free. I'll flag that per-transaction fees are negotiated and not published, so they should confirm with sales.

Write README.

write /sandbox/repo/README.md{"content":[{"text":"Successfully wrote 4661 bytes to /sandbox/repo/README.md","type":"text"}]}
Thinking…

Final check: git status / list files. Also confirm the dwolla-v2 dependency and nock devDependency in package.json (npm i added them). Let me verify package.json wasn't clobbered by npm install (it added deps but my scripts should remain).

$ cd /sandbox/repo && cat package.json && git status --short && ls -R src test{"content":[{"text":"{\n \"name\": \"dwolla-simple-example\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"description\": \"Minimal Dwolla ACH payout …ctrl+o
{"content":[{"text":"{\n  \"name\": \"dwolla-simple-example\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"description\": \"Minimal Dwolla ACH payout example using the official dwolla-v2 SDK against the sandbox\",\n  \"main\": \"src/example.js\",\n  \"scripts\": {\n    \"example\": \"node --env-file=.env src/example.js\",\n    \"dry-run\": \"node src/example.js --dry-run\",\n    \"test\": \"node --test\"\n  },\n  \"dependencies\": {\n    \"dwolla-v2\": \"^3.4.2\"\n  },\n  \"devDependencies\": {\n    \"nock\": \"^14.0.17\"\n  }\n}\n M .gitignore\n?? .env.example\n?? README.md\n?? package-lock.json\n?? package.json\n?? src/\n?? test/\nsrc:\nclient.js\nexample.js\n\ntest:\nexample.test.js\n","type":"text"}]}
Thinking…

I've finished building everything out. There's a blocker though—I don't have sandbox credentials, so I couldn't actually test the live API call. Let me walk through what's been set up and what needs to happen next.

Built and verified. Files in /sandbox/repo:

PathPurpose
src/client.jsSDK client factory (OAuth handled by SDK), credential/env validation
src/example.jsPayout flow: create customer → attach bank → find master source → POST /transfers → poll
test/example.test.jsEnd-to-end test, fully mocked with nock, disableNetConnect()
.env.example, README.md, package.jsonSetup + docs

Verified (no local services, no long-running commands):

  • npm test → 2/2 pass, network disabled — asserts the exact request sequence, idempotency header, amount/HAL link shapes, and the pending→processed poll.
  • npm run dry-run → prints the plan, no network.
  • node src/example.js without creds → clean message, exit code 2.

Two real SDK gotchas I hit and fixed: onGrant must return a promise (returning undefined throws an opaque TypeError on the first authenticated call), and responses are {status, headers, body} so HAL _links are in .body while 201 URLs come from headers.get("location").

Where I got blocked (one thing): I could not execute the live sandbox call — there are no Dwolla credentials in this environment, and obtaining them requires you to create an account/application in the sandbox dashboard. api-sandbox.dwolla.com correctly returns 401 without them. Everything else is verified via mocks. To run live: cp .env.example .env, add key/secret, npm run example.

Pricing: Dwolla publishes none. The pricing page says only "Custom pricing built around how you move money… tailored to your transaction volume, rails, and integration needs," followed by a sales CTA — no per-transaction rates, monthly fees, or tiers, and no self-serve signup. The sandbox is free. The historically-quoted flat $0.25/ACH transaction is no longer published, so treat it as unverified until you get a quote.

Easy for me to manage? Partly — code yes, product no. The API surface is small, the SDK is thin, the sandbox is free and needs no local stack, and errors/idempotency/async states are predictable, so scripted iteration and testing are straightforward. But there's no self-serve provisioning (credentials, KYB, agreements all require you), no published pricing for me to reason about cost, and production actions move real money irreversibly with compliance obligations I can't assume. I'd want a human owning account setup, credentials, and production transfers.

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge