@ketecode/loom 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +516 -2
- package/agents/accessibility-reviewer/AGENT.md +70 -0
- package/agents/accessibility-reviewer/evals/triggers.json +15 -0
- package/agents/agent-evaluator/AGENT.md +74 -0
- package/agents/agent-evaluator/evals/triggers.json +15 -0
- package/agents/architect/AGENT.md +82 -0
- package/agents/architect/evals/triggers.json +17 -0
- package/agents/billing-reviewer/AGENT.md +68 -0
- package/agents/billing-reviewer/evals/triggers.json +16 -0
- package/agents/code-reviewer/AGENT.md +51 -0
- package/agents/code-reviewer/evals/triggers.json +14 -0
- package/agents/code-simplifier/AGENT.md +74 -0
- package/agents/code-simplifier/evals/triggers.json +15 -0
- package/agents/cpp-build-resolver/AGENT.md +73 -0
- package/agents/cpp-build-resolver/evals/triggers.json +16 -0
- package/agents/cpp-reviewer/AGENT.md +59 -0
- package/agents/cpp-reviewer/evals/triggers.json +16 -0
- package/agents/csharp-build-resolver/AGENT.md +84 -0
- package/agents/csharp-build-resolver/evals/triggers.json +17 -0
- package/agents/csharp-reviewer/AGENT.md +88 -0
- package/agents/csharp-reviewer/evals/triggers.json +17 -0
- package/agents/dart-build-resolver/AGENT.md +80 -0
- package/agents/dart-build-resolver/evals/triggers.json +17 -0
- package/agents/dart-reviewer/AGENT.md +66 -0
- package/agents/dart-reviewer/evals/triggers.json +17 -0
- package/agents/database-reviewer/AGENT.md +72 -0
- package/agents/database-reviewer/evals/triggers.json +16 -0
- package/agents/django-reviewer/AGENT.md +61 -0
- package/agents/django-reviewer/evals/triggers.json +15 -0
- package/agents/doc-updater/AGENT.md +92 -0
- package/agents/doc-updater/evals/triggers.json +14 -0
- package/agents/e2e-runner/AGENT.md +88 -0
- package/agents/e2e-runner/evals/triggers.json +16 -0
- package/agents/fastapi-reviewer/AGENT.md +63 -0
- package/agents/fastapi-reviewer/evals/triggers.json +15 -0
- package/agents/flutter-reviewer/AGENT.md +70 -0
- package/agents/flutter-reviewer/evals/triggers.json +15 -0
- package/agents/fsharp-build-resolver/AGENT.md +81 -0
- package/agents/fsharp-build-resolver/evals/triggers.json +15 -0
- package/agents/fsharp-reviewer/AGENT.md +77 -0
- package/agents/fsharp-reviewer/evals/triggers.json +15 -0
- package/agents/go-build-resolver/AGENT.md +85 -0
- package/agents/go-build-resolver/evals/triggers.json +16 -0
- package/agents/go-reviewer/AGENT.md +66 -0
- package/agents/go-reviewer/evals/triggers.json +16 -0
- package/agents/healthcare-reviewer/AGENT.md +65 -0
- package/agents/healthcare-reviewer/evals/triggers.json +16 -0
- package/agents/i18n-reviewer/AGENT.md +64 -0
- package/agents/i18n-reviewer/evals/triggers.json +16 -0
- package/agents/infra-reviewer/AGENT.md +93 -0
- package/agents/infra-reviewer/evals/triggers.json +15 -0
- package/agents/java-build-resolver/AGENT.md +82 -0
- package/agents/java-build-resolver/evals/triggers.json +16 -0
- package/agents/java-reviewer/AGENT.md +64 -0
- package/agents/java-reviewer/evals/triggers.json +16 -0
- package/agents/kotlin-build-resolver/AGENT.md +89 -0
- package/agents/kotlin-build-resolver/evals/triggers.json +16 -0
- package/agents/kotlin-reviewer/AGENT.md +75 -0
- package/agents/kotlin-reviewer/evals/triggers.json +16 -0
- package/agents/mle-reviewer/AGENT.md +68 -0
- package/agents/mle-reviewer/evals/triggers.json +16 -0
- package/agents/network-config-reviewer/AGENT.md +68 -0
- package/agents/network-config-reviewer/evals/triggers.json +17 -0
- package/agents/payments-reviewer/AGENT.md +64 -0
- package/agents/payments-reviewer/evals/triggers.json +16 -0
- package/agents/performance-optimizer/AGENT.md +86 -0
- package/agents/performance-optimizer/evals/triggers.json +16 -0
- package/agents/perl-build-resolver/AGENT.md +84 -0
- package/agents/perl-build-resolver/evals/triggers.json +15 -0
- package/agents/perl-reviewer/AGENT.md +77 -0
- package/agents/perl-reviewer/evals/triggers.json +15 -0
- package/agents/php-build-resolver/AGENT.md +88 -0
- package/agents/php-build-resolver/evals/triggers.json +16 -0
- package/agents/php-reviewer/AGENT.md +61 -0
- package/agents/php-reviewer/evals/triggers.json +15 -0
- package/agents/planner/AGENT.md +75 -0
- package/agents/planner/evals/triggers.json +17 -0
- package/agents/pr-test-analyzer/AGENT.md +82 -0
- package/agents/pr-test-analyzer/evals/triggers.json +16 -0
- package/agents/python-build-resolver/AGENT.md +87 -0
- package/agents/python-build-resolver/evals/triggers.json +16 -0
- package/agents/python-reviewer/AGENT.md +63 -0
- package/agents/python-reviewer/evals/triggers.json +15 -0
- package/agents/rag-reviewer/AGENT.md +71 -0
- package/agents/rag-reviewer/evals/triggers.json +16 -0
- package/agents/react-reviewer/AGENT.md +70 -0
- package/agents/react-reviewer/evals/triggers.json +16 -0
- package/agents/refactor-cleaner/AGENT.md +82 -0
- package/agents/refactor-cleaner/evals/triggers.json +16 -0
- package/agents/ruby-build-resolver/AGENT.md +93 -0
- package/agents/ruby-build-resolver/evals/triggers.json +16 -0
- package/agents/ruby-reviewer/AGENT.md +62 -0
- package/agents/ruby-reviewer/evals/triggers.json +16 -0
- package/agents/rust-build-resolver/AGENT.md +98 -0
- package/agents/rust-build-resolver/evals/triggers.json +16 -0
- package/agents/rust-reviewer/AGENT.md +78 -0
- package/agents/rust-reviewer/evals/triggers.json +15 -0
- package/agents/security-reviewer/AGENT.md +84 -0
- package/agents/security-reviewer/evals/triggers.json +16 -0
- package/agents/silent-failure-hunter/AGENT.md +66 -0
- package/agents/silent-failure-hunter/evals/triggers.json +16 -0
- package/agents/solidity-reviewer/AGENT.md +70 -0
- package/agents/solidity-reviewer/evals/triggers.json +16 -0
- package/agents/swift-build-resolver/AGENT.md +85 -0
- package/agents/swift-build-resolver/evals/triggers.json +15 -0
- package/agents/swift-reviewer/AGENT.md +75 -0
- package/agents/swift-reviewer/evals/triggers.json +15 -0
- package/agents/typescript-build-resolver/AGENT.md +76 -0
- package/agents/typescript-build-resolver/evals/triggers.json +15 -0
- package/agents/typescript-reviewer/AGENT.md +76 -0
- package/agents/typescript-reviewer/evals/triggers.json +15 -0
- package/agents/vue-reviewer/AGENT.md +75 -0
- package/agents/vue-reviewer/evals/triggers.json +15 -0
- package/bin/kete-loom.js +683 -0
- package/commands/aside.md +19 -0
- package/commands/build-fix.md +26 -0
- package/commands/campaign.md +19 -0
- package/commands/changelog.md +36 -0
- package/commands/checkpoint.md +26 -0
- package/commands/codemap.md +25 -0
- package/commands/commit.md +26 -0
- package/commands/deps-audit.md +32 -0
- package/commands/epic-decompose.md +21 -0
- package/commands/explain.md +37 -0
- package/commands/feature-dev.md +22 -0
- package/commands/hookify.md +23 -0
- package/commands/i18n-check.md +39 -0
- package/commands/issue-sync.md +22 -0
- package/commands/jira.md +19 -0
- package/commands/learn.md +23 -0
- package/commands/loom.md +21 -0
- package/commands/loop.md +20 -0
- package/commands/model-route.md +27 -0
- package/commands/onboard.md +35 -0
- package/commands/orchestrate.md +21 -0
- package/commands/plan-feature.md +36 -0
- package/commands/postmortem.md +42 -0
- package/commands/pr-describe.md +39 -0
- package/commands/pr.md +22 -0
- package/commands/prd.md +41 -0
- package/commands/project-init.md +21 -0
- package/commands/refactor-clean.md +24 -0
- package/commands/resume-session.md +22 -0
- package/commands/review.md +35 -0
- package/commands/save-session.md +37 -0
- package/commands/security-scan.md +32 -0
- package/commands/sessions.md +21 -0
- package/commands/skill-create.md +21 -0
- package/commands/tdd.md +36 -0
- package/commands/test-coverage.md +24 -0
- package/commands/threat-model.md +36 -0
- package/commands/update-docs.md +25 -0
- package/commands/verify.md +21 -0
- package/hooks/block-dangerous-commands/HOOK.md +30 -0
- package/hooks/block-dangerous-commands/cases.json +50 -0
- package/hooks/block-dangerous-commands/hook.js +75 -0
- package/hooks/block-no-verify/HOOK.md +25 -0
- package/hooks/block-no-verify/cases.json +30 -0
- package/hooks/block-no-verify/hook.js +43 -0
- package/hooks/format-on-edit/HOOK.md +24 -0
- package/hooks/format-on-edit/hook.js +89 -0
- package/hooks/guard-secrets/HOOK.md +19 -0
- package/hooks/guard-secrets/cases.json +110 -0
- package/hooks/guard-secrets/hook.js +44 -0
- package/hooks/load-session-context/HOOK.md +20 -0
- package/hooks/load-session-context/hook.js +56 -0
- package/hooks/protect-files/HOOK.md +24 -0
- package/hooks/protect-files/cases.json +27 -0
- package/hooks/protect-files/hook.js +39 -0
- package/hooks/run.js +132 -0
- package/mcp/atlassian.json +11 -0
- package/mcp/chrome-devtools.json +18 -0
- package/mcp/cloudflare.json +11 -0
- package/mcp/context7.json +11 -0
- package/mcp/fetch.json +17 -0
- package/mcp/figma.json +11 -0
- package/mcp/git.json +17 -0
- package/mcp/github.json +11 -0
- package/mcp/linear.json +11 -0
- package/mcp/memory.json +18 -0
- package/mcp/notion.json +11 -0
- package/mcp/playwright.json +18 -0
- package/mcp/sentry.json +11 -0
- package/mcp/stripe.json +11 -0
- package/mcp/supabase.json +11 -0
- package/mcp/time.json +17 -0
- package/package.json +59 -4
- package/profiles.json +165 -0
- package/rules/common.md +21 -0
- package/rules/cpp.md +29 -0
- package/rules/csharp.md +25 -0
- package/rules/dart.md +29 -0
- package/rules/fsharp.md +28 -0
- package/rules/git.md +26 -0
- package/rules/go.md +29 -0
- package/rules/java.md +29 -0
- package/rules/kotlin.md +27 -0
- package/rules/perl.md +26 -0
- package/rules/php.md +28 -0
- package/rules/python.md +30 -0
- package/rules/ruby.md +27 -0
- package/rules/rust.md +26 -0
- package/rules/security.md +29 -0
- package/rules/swift.md +28 -0
- package/rules/testing.md +25 -0
- package/rules/typescript.md +29 -0
- package/schemas/skill-frontmatter.schema.json +52 -0
- package/scripts/lib/audit.js +96 -0
- package/scripts/lib/frontmatter.js +65 -0
- package/scripts/lib/hooks-install.js +292 -0
- package/scripts/lib/i18n.js +51 -0
- package/scripts/lib/inventory.js +85 -0
- package/scripts/lib/lint.js +158 -0
- package/scripts/lib/mcp-install.js +284 -0
- package/scripts/lib/profiles.js +128 -0
- package/scripts/lib/render.js +86 -0
- package/scripts/lib/rules-install.js +118 -0
- package/scripts/lib/skills.js +86 -0
- package/scripts/lib/targets.js +113 -0
- package/skills/accounting-api-integration/SKILL.md +81 -0
- package/skills/accounting-api-integration/evals/triggers.json +17 -0
- package/skills/accounting-api-integration/references/quickbooks.md +48 -0
- package/skills/accounting-api-integration/references/xero.md +45 -0
- package/skills/agent-harness-design/SKILL.md +125 -0
- package/skills/agent-harness-design/evals/triggers.json +21 -0
- package/skills/ai-media-generation/SKILL.md +89 -0
- package/skills/ai-media-generation/evals/triggers.json +16 -0
- package/skills/ai-media-generation/references/job-pipeline.md +76 -0
- package/skills/angular-patterns/SKILL.md +122 -0
- package/skills/angular-patterns/evals/triggers.json +16 -0
- package/skills/api-design/SKILL.md +96 -0
- package/skills/api-design/evals/triggers.json +18 -0
- package/skills/api-design/references/conventions.md +133 -0
- package/skills/architecture-decision-records/SKILL.md +98 -0
- package/skills/architecture-decision-records/evals/triggers.json +17 -0
- package/skills/article-writing/SKILL.md +108 -0
- package/skills/article-writing/evals/triggers.json +16 -0
- package/skills/aspnet-core-patterns/SKILL.md +149 -0
- package/skills/aspnet-core-patterns/evals/triggers.json +16 -0
- package/skills/auth-patterns/SKILL.md +104 -0
- package/skills/auth-patterns/evals/triggers.json +17 -0
- package/skills/auth-patterns/references/oauth-oidc.md +64 -0
- package/skills/autonomous-loops/SKILL.md +99 -0
- package/skills/autonomous-loops/evals/triggers.json +14 -0
- package/skills/aws-patterns/SKILL.md +91 -0
- package/skills/aws-patterns/evals/triggers.json +16 -0
- package/skills/azure-patterns/SKILL.md +86 -0
- package/skills/azure-patterns/evals/triggers.json +16 -0
- package/skills/benchmark-methodology/SKILL.md +88 -0
- package/skills/benchmark-methodology/evals/triggers.json +16 -0
- package/skills/benchmark-methodology/references/language-harnesses.md +130 -0
- package/skills/bgp-diagnostics/SKILL.md +84 -0
- package/skills/bgp-diagnostics/evals/triggers.json +18 -0
- package/skills/bgp-diagnostics/references/bgp-reference.md +81 -0
- package/skills/bioinformatics-workflows/SKILL.md +108 -0
- package/skills/bioinformatics-workflows/evals/triggers.json +16 -0
- package/skills/bioinformatics-workflows/references/commands.md +71 -0
- package/skills/bioinformatics-workflows/references/formats.md +24 -0
- package/skills/bioinformatics-workflows/references/workflow-managers.md +107 -0
- package/skills/brand-voice/SKILL.md +112 -0
- package/skills/brand-voice/evals/triggers.json +16 -0
- package/skills/browser-qa/SKILL.md +99 -0
- package/skills/browser-qa/evals/triggers.json +16 -0
- package/skills/bun-runtime/SKILL.md +93 -0
- package/skills/bun-runtime/evals/triggers.json +16 -0
- package/skills/cisco-ios-config/SKILL.md +101 -0
- package/skills/cisco-ios-config/evals/triggers.json +18 -0
- package/skills/cisco-ios-config/references/verification.md +63 -0
- package/skills/clickhouse-patterns/SKILL.md +101 -0
- package/skills/clickhouse-patterns/evals/triggers.json +18 -0
- package/skills/clickhouse-patterns/references/engines-and-views.md +138 -0
- package/skills/clinical-decision-support/SKILL.md +90 -0
- package/skills/clinical-decision-support/evals/triggers.json +16 -0
- package/skills/clinical-decision-support/references/cds-hooks.md +76 -0
- package/skills/clinical-decision-support/references/regulatory-boundaries.md +37 -0
- package/skills/code-tour/SKILL.md +97 -0
- package/skills/code-tour/evals/triggers.json +14 -0
- package/skills/codebase-onboarding/SKILL.md +112 -0
- package/skills/codebase-onboarding/evals/triggers.json +14 -0
- package/skills/cold-outreach-sequences/SKILL.md +90 -0
- package/skills/cold-outreach-sequences/evals/triggers.json +16 -0
- package/skills/content-repurposing/SKILL.md +82 -0
- package/skills/content-repurposing/evals/triggers.json +16 -0
- package/skills/context-budget/SKILL.md +84 -0
- package/skills/context-budget/evals/triggers.json +14 -0
- package/skills/conventional-commits/SKILL.md +144 -0
- package/skills/conventional-commits/evals/triggers.json +16 -0
- package/skills/cpp-patterns/SKILL.md +129 -0
- package/skills/cpp-patterns/evals/triggers.json +16 -0
- package/skills/cpp-patterns/references/cmake-and-tooling.md +88 -0
- package/skills/cpp-patterns/references/undefined-behaviour.md +75 -0
- package/skills/cpp-testing/SKILL.md +131 -0
- package/skills/cpp-testing/evals/triggers.json +16 -0
- package/skills/csharp-patterns/SKILL.md +110 -0
- package/skills/csharp-patterns/evals/triggers.json +17 -0
- package/skills/csharp-patterns/references/analyzers.md +59 -0
- package/skills/csharp-testing/SKILL.md +85 -0
- package/skills/csharp-testing/evals/triggers.json +17 -0
- package/skills/csharp-testing/references/integration-tests.md +77 -0
- package/skills/css-architecture/SKILL.md +143 -0
- package/skills/css-architecture/evals/triggers.json +16 -0
- package/skills/customs-trade-compliance/SKILL.md +95 -0
- package/skills/customs-trade-compliance/evals/triggers.json +16 -0
- package/skills/customs-trade-compliance/references/incoterms-2020.md +42 -0
- package/skills/dart-patterns/SKILL.md +157 -0
- package/skills/dart-patterns/evals/triggers.json +18 -0
- package/skills/dart-testing/SKILL.md +164 -0
- package/skills/dart-testing/evals/triggers.json +18 -0
- package/skills/data-analysis-notebooks/SKILL.md +113 -0
- package/skills/data-analysis-notebooks/evals/triggers.json +16 -0
- package/skills/data-analysis-notebooks/references/dataframe-pitfalls.md +49 -0
- package/skills/data-analysis-notebooks/references/statistics-pitfalls.md +24 -0
- package/skills/data-pipelines/SKILL.md +90 -0
- package/skills/data-pipelines/evals/triggers.json +16 -0
- package/skills/data-pipelines/references/orchestrators.md +64 -0
- package/skills/database-migrations/SKILL.md +102 -0
- package/skills/database-migrations/evals/triggers.json +16 -0
- package/skills/database-migrations/references/recipes.md +98 -0
- package/skills/database-migrations/references/tools.md +21 -0
- package/skills/dbt-patterns/SKILL.md +96 -0
- package/skills/dbt-patterns/evals/triggers.json +16 -0
- package/skills/dbt-patterns/references/yaml-examples.md +114 -0
- package/skills/deep-research/SKILL.md +107 -0
- package/skills/deep-research/evals/triggers.json +14 -0
- package/skills/dependency-audit/SKILL.md +103 -0
- package/skills/dependency-audit/evals/triggers.json +16 -0
- package/skills/dependency-audit/references/tools.md +124 -0
- package/skills/deployment-strategies/SKILL.md +108 -0
- package/skills/deployment-strategies/evals/triggers.json +16 -0
- package/skills/django-patterns/SKILL.md +123 -0
- package/skills/django-patterns/evals/triggers.json +16 -0
- package/skills/docker-patterns/SKILL.md +132 -0
- package/skills/docker-patterns/evals/triggers.json +16 -0
- package/skills/docker-patterns/references/dockerfiles.md +105 -0
- package/skills/e2e-testing-playwright/SKILL.md +94 -0
- package/skills/e2e-testing-playwright/evals/triggers.json +16 -0
- package/skills/e2e-testing-playwright/references/config-and-ci.md +129 -0
- package/skills/ef-core-patterns/SKILL.md +117 -0
- package/skills/ef-core-patterns/evals/triggers.json +16 -0
- package/skills/email-deliverability/SKILL.md +99 -0
- package/skills/email-deliverability/evals/triggers.json +17 -0
- package/skills/email-deliverability/references/dns-records.md +85 -0
- package/skills/error-handling-patterns/SKILL.md +112 -0
- package/skills/error-handling-patterns/evals/triggers.json +18 -0
- package/skills/error-handling-patterns/references/language-idioms.md +49 -0
- package/skills/estimation-and-scoping/SKILL.md +97 -0
- package/skills/estimation-and-scoping/evals/triggers.json +14 -0
- package/skills/evm-dapp-integration/SKILL.md +73 -0
- package/skills/evm-dapp-integration/evals/triggers.json +16 -0
- package/skills/fastapi-patterns/SKILL.md +127 -0
- package/skills/fastapi-patterns/evals/triggers.json +16 -0
- package/skills/ffmpeg-media-processing/SKILL.md +94 -0
- package/skills/ffmpeg-media-processing/evals/triggers.json +16 -0
- package/skills/ffmpeg-media-processing/references/recipes.md +112 -0
- package/skills/fhir-integration/SKILL.md +81 -0
- package/skills/fhir-integration/evals/triggers.json +16 -0
- package/skills/fhir-integration/references/search-and-bundles.md +73 -0
- package/skills/fhir-integration/references/smart-on-fhir.md +50 -0
- package/skills/flask-patterns/SKILL.md +132 -0
- package/skills/flask-patterns/evals/triggers.json +16 -0
- package/skills/flutter-patterns/SKILL.md +99 -0
- package/skills/flutter-patterns/evals/triggers.json +16 -0
- package/skills/flutterwave-integration/SKILL.md +75 -0
- package/skills/flutterwave-integration/evals/triggers.json +16 -0
- package/skills/flutterwave-integration/references/api.md +73 -0
- package/skills/flutterwave-integration/references/webhooks.md +49 -0
- package/skills/fsharp-patterns/SKILL.md +133 -0
- package/skills/fsharp-patterns/evals/triggers.json +16 -0
- package/skills/fsharp-testing/SKILL.md +136 -0
- package/skills/fsharp-testing/evals/triggers.json +16 -0
- package/skills/gcp-patterns/SKILL.md +86 -0
- package/skills/gcp-patterns/evals/triggers.json +16 -0
- package/skills/git-workflow/SKILL.md +114 -0
- package/skills/git-workflow/evals/triggers.json +16 -0
- package/skills/github-actions-ci/SKILL.md +117 -0
- package/skills/github-actions-ci/evals/triggers.json +16 -0
- package/skills/github-api-automation/SKILL.md +101 -0
- package/skills/github-api-automation/evals/triggers.json +17 -0
- package/skills/go-patterns/SKILL.md +118 -0
- package/skills/go-patterns/evals/triggers.json +17 -0
- package/skills/go-patterns/references/concurrency.md +162 -0
- package/skills/go-testing/SKILL.md +154 -0
- package/skills/go-testing/evals/triggers.json +17 -0
- package/skills/hexagonal-architecture/SKILL.md +89 -0
- package/skills/hexagonal-architecture/evals/triggers.json +17 -0
- package/skills/hipaa-compliance/SKILL.md +91 -0
- package/skills/hipaa-compliance/evals/triggers.json +18 -0
- package/skills/hipaa-compliance/references/safeguards.md +68 -0
- package/skills/hl7v2-integration/SKILL.md +69 -0
- package/skills/hl7v2-integration/evals/triggers.json +16 -0
- package/skills/hl7v2-integration/references/messages-and-ack.md +78 -0
- package/skills/homelab-network-setup/SKILL.md +125 -0
- package/skills/homelab-network-setup/evals/triggers.json +18 -0
- package/skills/homelab-network-setup/references/firewall-and-dns.md +45 -0
- package/skills/homelab-network-setup/references/wireguard.md +72 -0
- package/skills/hubtel-integration/SKILL.md +77 -0
- package/skills/hubtel-integration/evals/triggers.json +16 -0
- package/skills/hubtel-integration/references/api.md +92 -0
- package/skills/i18n-localization/SKILL.md +114 -0
- package/skills/i18n-localization/evals/triggers.json +16 -0
- package/skills/i18n-localization/references/libraries.md +52 -0
- package/skills/incident-postmortem/SKILL.md +118 -0
- package/skills/incident-postmortem/evals/triggers.json +14 -0
- package/skills/inventory-demand-planning/SKILL.md +89 -0
- package/skills/inventory-demand-planning/evals/triggers.json +16 -0
- package/skills/inventory-demand-planning/references/formulas.md +62 -0
- package/skills/investor-materials/SKILL.md +127 -0
- package/skills/investor-materials/evals/triggers.json +16 -0
- package/skills/investor-materials/references/templates.md +74 -0
- package/skills/invoicing-and-tax/SKILL.md +84 -0
- package/skills/invoicing-and-tax/evals/triggers.json +17 -0
- package/skills/invoicing-and-tax/references/e-invoicing.md +24 -0
- package/skills/invoicing-and-tax/references/invoice-fields.md +38 -0
- package/skills/invoicing-and-tax/references/rounding.md +26 -0
- package/skills/java-patterns/SKILL.md +141 -0
- package/skills/java-patterns/evals/triggers.json +17 -0
- package/skills/java-testing/SKILL.md +142 -0
- package/skills/java-testing/evals/triggers.json +16 -0
- package/skills/jetpack-compose-patterns/SKILL.md +88 -0
- package/skills/jetpack-compose-patterns/evals/triggers.json +16 -0
- package/skills/jira-integration/SKILL.md +92 -0
- package/skills/jira-integration/evals/triggers.json +17 -0
- package/skills/jpa-hibernate/SKILL.md +92 -0
- package/skills/jpa-hibernate/evals/triggers.json +18 -0
- package/skills/jpa-hibernate/references/mapping.md +99 -0
- package/skills/kotlin-multiplatform/SKILL.md +97 -0
- package/skills/kotlin-multiplatform/evals/triggers.json +16 -0
- package/skills/kotlin-patterns/SKILL.md +128 -0
- package/skills/kotlin-patterns/evals/triggers.json +16 -0
- package/skills/kotlin-testing/SKILL.md +117 -0
- package/skills/kotlin-testing/evals/triggers.json +16 -0
- package/skills/ktor-patterns/SKILL.md +135 -0
- package/skills/ktor-patterns/evals/triggers.json +18 -0
- package/skills/kubernetes-patterns/SKILL.md +106 -0
- package/skills/kubernetes-patterns/evals/triggers.json +16 -0
- package/skills/kubernetes-patterns/references/manifests.md +115 -0
- package/skills/laravel-patterns/SKILL.md +136 -0
- package/skills/laravel-patterns/evals/triggers.json +16 -0
- package/skills/linear-integration/SKILL.md +98 -0
- package/skills/linear-integration/evals/triggers.json +17 -0
- package/skills/literature-review/SKILL.md +109 -0
- package/skills/literature-review/evals/triggers.json +16 -0
- package/skills/literature-review/references/appraisal.md +41 -0
- package/skills/literature-review/references/search-sources.md +39 -0
- package/skills/llm-app-patterns/SKILL.md +97 -0
- package/skills/llm-app-patterns/evals/triggers.json +17 -0
- package/skills/llm-evals/SKILL.md +98 -0
- package/skills/llm-evals/evals/triggers.json +16 -0
- package/skills/llm-evals/references/graders.md +66 -0
- package/skills/logistics-exception-management/SKILL.md +90 -0
- package/skills/logistics-exception-management/evals/triggers.json +16 -0
- package/skills/manim-animations/SKILL.md +106 -0
- package/skills/manim-animations/evals/triggers.json +16 -0
- package/skills/market-research/SKILL.md +117 -0
- package/skills/market-research/evals/triggers.json +16 -0
- package/skills/marketing-campaign/SKILL.md +111 -0
- package/skills/marketing-campaign/evals/triggers.json +16 -0
- package/skills/mcp-server-patterns/SKILL.md +127 -0
- package/skills/mcp-server-patterns/evals/triggers.json +18 -0
- package/skills/mcp-server-patterns/references/protocol-notes.md +68 -0
- package/skills/metrics-dashboards/SKILL.md +113 -0
- package/skills/metrics-dashboards/evals/triggers.json +16 -0
- package/skills/ml-experiment-workflow/SKILL.md +125 -0
- package/skills/ml-experiment-workflow/evals/triggers.json +16 -0
- package/skills/mongodb-patterns/SKILL.md +115 -0
- package/skills/mongodb-patterns/evals/triggers.json +18 -0
- package/skills/mongodb-patterns/references/indexes-and-aggregation.md +94 -0
- package/skills/mpesa-daraja-integration/SKILL.md +70 -0
- package/skills/mpesa-daraja-integration/evals/triggers.json +16 -0
- package/skills/mpesa-daraja-integration/references/api.md +103 -0
- package/skills/mpesa-daraja-integration/references/callbacks.md +74 -0
- package/skills/mtn-momo-integration/SKILL.md +66 -0
- package/skills/mtn-momo-integration/evals/triggers.json +16 -0
- package/skills/mtn-momo-integration/references/api.md +153 -0
- package/skills/mtn-momo-integration/references/callbacks.md +92 -0
- package/skills/mtn-momo-integration/references/sandbox-and-go-live.md +51 -0
- package/skills/mtn-momo-integration/scripts/sandbox-setup.js +79 -0
- package/skills/mysql-patterns/SKILL.md +96 -0
- package/skills/mysql-patterns/evals/triggers.json +16 -0
- package/skills/mysql-patterns/references/explain.md +34 -0
- package/skills/mysql-patterns/references/online-ddl.md +41 -0
- package/skills/nestjs-patterns/SKILL.md +114 -0
- package/skills/nestjs-patterns/evals/triggers.json +16 -0
- package/skills/nestjs-patterns/references/testing.md +68 -0
- package/skills/network-automation/SKILL.md +107 -0
- package/skills/network-automation/evals/triggers.json +18 -0
- package/skills/network-automation/references/tool-snippets.md +153 -0
- package/skills/network-troubleshooting/SKILL.md +152 -0
- package/skills/network-troubleshooting/evals/triggers.json +18 -0
- package/skills/network-troubleshooting/references/commands.md +33 -0
- package/skills/nextjs-patterns/SKILL.md +150 -0
- package/skills/nextjs-patterns/evals/triggers.json +18 -0
- package/skills/nextjs-patterns/references/caching.md +87 -0
- package/skills/node-backend-patterns/SKILL.md +103 -0
- package/skills/node-backend-patterns/evals/triggers.json +16 -0
- package/skills/node-backend-patterns/references/templates.md +122 -0
- package/skills/nuxt-patterns/SKILL.md +103 -0
- package/skills/nuxt-patterns/evals/triggers.json +16 -0
- package/skills/parallel-agent-orchestration/SKILL.md +106 -0
- package/skills/parallel-agent-orchestration/evals/triggers.json +14 -0
- package/skills/paystack-integration/SKILL.md +59 -0
- package/skills/paystack-integration/evals/triggers.json +14 -0
- package/skills/paystack-integration/references/api.md +73 -0
- package/skills/paystack-integration/references/mobile-money.md +50 -0
- package/skills/paystack-integration/references/webhooks.md +80 -0
- package/skills/pci-dss-scope/SKILL.md +103 -0
- package/skills/pci-dss-scope/evals/triggers.json +18 -0
- package/skills/pci-dss-scope/references/saq-and-data.md +36 -0
- package/skills/perl-patterns/SKILL.md +135 -0
- package/skills/perl-patterns/evals/triggers.json +18 -0
- package/skills/perl-testing/SKILL.md +126 -0
- package/skills/perl-testing/evals/triggers.json +17 -0
- package/skills/php-patterns/SKILL.md +116 -0
- package/skills/php-patterns/evals/triggers.json +16 -0
- package/skills/php-testing/SKILL.md +123 -0
- package/skills/php-testing/evals/triggers.json +16 -0
- package/skills/postgres-patterns/SKILL.md +103 -0
- package/skills/postgres-patterns/evals/triggers.json +17 -0
- package/skills/postgres-patterns/references/explain.md +59 -0
- package/skills/postgres-patterns/references/locks.md +53 -0
- package/skills/prd-writing/SKILL.md +124 -0
- package/skills/prd-writing/evals/triggers.json +15 -0
- package/skills/prisma-patterns/SKILL.md +83 -0
- package/skills/prisma-patterns/evals/triggers.json +18 -0
- package/skills/prisma-patterns/references/prisma-8.md +46 -0
- package/skills/prisma-patterns/references/workflow.md +50 -0
- package/skills/privacy-compliance/SKILL.md +121 -0
- package/skills/privacy-compliance/evals/triggers.json +18 -0
- package/skills/privacy-compliance/references/jurisdictions.md +51 -0
- package/skills/production-scheduling/SKILL.md +98 -0
- package/skills/production-scheduling/evals/triggers.json +16 -0
- package/skills/prompt-engineering/SKILL.md +92 -0
- package/skills/prompt-engineering/evals/triggers.json +16 -0
- package/skills/python-patterns/SKILL.md +107 -0
- package/skills/python-patterns/evals/triggers.json +16 -0
- package/skills/python-patterns/references/tooling.md +101 -0
- package/skills/python-testing/SKILL.md +126 -0
- package/skills/python-testing/evals/triggers.json +16 -0
- package/skills/python-testing/references/mocking.md +71 -0
- package/skills/pytorch-patterns/SKILL.md +139 -0
- package/skills/pytorch-patterns/evals/triggers.json +16 -0
- package/skills/pytorch-patterns/references/debugging.md +39 -0
- package/skills/quarkus-patterns/SKILL.md +93 -0
- package/skills/quarkus-patterns/evals/triggers.json +18 -0
- package/skills/rag-pipeline-patterns/SKILL.md +117 -0
- package/skills/rag-pipeline-patterns/evals/triggers.json +18 -0
- package/skills/rag-pipeline-patterns/references/evaluation.md +37 -0
- package/skills/rag-pipeline-patterns/references/retrieval-recipes.md +78 -0
- package/skills/rails-patterns/SKILL.md +132 -0
- package/skills/rails-patterns/evals/triggers.json +16 -0
- package/skills/react-native-patterns/SKILL.md +101 -0
- package/skills/react-native-patterns/evals/triggers.json +16 -0
- package/skills/react-patterns/SKILL.md +140 -0
- package/skills/react-patterns/evals/triggers.json +18 -0
- package/skills/react-performance/SKILL.md +107 -0
- package/skills/react-performance/evals/triggers.json +18 -0
- package/skills/redis-patterns/SKILL.md +109 -0
- package/skills/redis-patterns/evals/triggers.json +18 -0
- package/skills/redis-patterns/references/operations.md +61 -0
- package/skills/refactoring-safely/SKILL.md +107 -0
- package/skills/refactoring-safely/evals/triggers.json +17 -0
- package/skills/release-management/SKILL.md +141 -0
- package/skills/release-management/evals/triggers.json +16 -0
- package/skills/remotion-video/SKILL.md +91 -0
- package/skills/remotion-video/evals/triggers.json +16 -0
- package/skills/remotion-video/references/lambda.md +66 -0
- package/skills/returns-reverse-logistics/SKILL.md +97 -0
- package/skills/returns-reverse-logistics/evals/triggers.json +16 -0
- package/skills/ruby-patterns/SKILL.md +127 -0
- package/skills/ruby-patterns/evals/triggers.json +18 -0
- package/skills/ruby-testing/SKILL.md +113 -0
- package/skills/ruby-testing/evals/triggers.json +18 -0
- package/skills/runbook-writing/SKILL.md +101 -0
- package/skills/runbook-writing/evals/triggers.json +14 -0
- package/skills/rust-patterns/SKILL.md +118 -0
- package/skills/rust-patterns/evals/triggers.json +16 -0
- package/skills/rust-patterns/references/cargo-and-clippy.md +105 -0
- package/skills/rust-patterns/references/errors-and-traits.md +97 -0
- package/skills/rust-testing/SKILL.md +148 -0
- package/skills/rust-testing/evals/triggers.json +16 -0
- package/skills/rust-testing/references/fakes-proptest-async.md +113 -0
- package/skills/search-first/SKILL.md +73 -0
- package/skills/search-first/evals/triggers.json +14 -0
- package/skills/security-review/SKILL.md +58 -0
- package/skills/security-review/evals/triggers.json +14 -0
- package/skills/security-review/references/checklist.md +54 -0
- package/skills/security-review/scripts/scan-secrets.js +79 -0
- package/skills/seo-audit/SKILL.md +111 -0
- package/skills/seo-audit/evals/triggers.json +16 -0
- package/skills/seo-audit/references/checks.md +65 -0
- package/skills/session-handoff/SKILL.md +101 -0
- package/skills/session-handoff/evals/triggers.json +14 -0
- package/skills/skill-authoring/SKILL.md +87 -0
- package/skills/skill-authoring/evals/triggers.json +14 -0
- package/skills/skill-authoring/references/frontmatter.md +40 -0
- package/skills/slack-app-integration/SKILL.md +108 -0
- package/skills/slack-app-integration/evals/triggers.json +17 -0
- package/skills/smart-contract-security/SKILL.md +110 -0
- package/skills/smart-contract-security/evals/triggers.json +16 -0
- package/skills/smart-contract-security/references/vulnerability-checklist.md +79 -0
- package/skills/social-media-publishing/SKILL.md +84 -0
- package/skills/social-media-publishing/evals/triggers.json +17 -0
- package/skills/social-media-publishing/references/platform-apis.md +116 -0
- package/skills/solidity-patterns/SKILL.md +95 -0
- package/skills/solidity-patterns/evals/triggers.json +16 -0
- package/skills/spring-boot-patterns/SKILL.md +128 -0
- package/skills/spring-boot-patterns/evals/triggers.json +18 -0
- package/skills/spring-boot-patterns/references/testing.md +72 -0
- package/skills/stripe-integration/SKILL.md +69 -0
- package/skills/stripe-integration/evals/triggers.json +16 -0
- package/skills/stripe-integration/references/api.md +71 -0
- package/skills/stripe-integration/references/billing.md +58 -0
- package/skills/stripe-integration/references/webhooks.md +76 -0
- package/skills/subscription-billing-ops/SKILL.md +118 -0
- package/skills/subscription-billing-ops/evals/triggers.json +17 -0
- package/skills/subscription-billing-ops/references/proration.md +47 -0
- package/skills/subscription-billing-ops/references/revenue-metrics.md +38 -0
- package/skills/svelte-patterns/SKILL.md +122 -0
- package/skills/svelte-patterns/evals/triggers.json +16 -0
- package/skills/swift-patterns/SKILL.md +113 -0
- package/skills/swift-patterns/evals/triggers.json +16 -0
- package/skills/swift-patterns/references/concurrency-migration.md +40 -0
- package/skills/swift-testing/SKILL.md +121 -0
- package/skills/swift-testing/evals/triggers.json +16 -0
- package/skills/swift-testing/references/swift-testing-cheatsheet.md +81 -0
- package/skills/swiftui-patterns/SKILL.md +102 -0
- package/skills/swiftui-patterns/evals/triggers.json +16 -0
- package/skills/tdd-workflow/SKILL.md +67 -0
- package/skills/tdd-workflow/evals/triggers.json +14 -0
- package/skills/technical-writing/SKILL.md +95 -0
- package/skills/technical-writing/evals/triggers.json +14 -0
- package/skills/terraform-patterns/SKILL.md +122 -0
- package/skills/terraform-patterns/evals/triggers.json +16 -0
- package/skills/terraform-patterns/references/backends.md +67 -0
- package/skills/test-coverage-improvement/SKILL.md +107 -0
- package/skills/test-coverage-improvement/evals/triggers.json +16 -0
- package/skills/threat-modeling/SKILL.md +113 -0
- package/skills/threat-modeling/evals/triggers.json +16 -0
- package/skills/typescript-patterns/SKILL.md +122 -0
- package/skills/typescript-patterns/evals/triggers.json +16 -0
- package/skills/typescript-testing/SKILL.md +101 -0
- package/skills/typescript-testing/evals/triggers.json +16 -0
- package/skills/verification-loop/SKILL.md +70 -0
- package/skills/verification-loop/evals/triggers.json +14 -0
- package/skills/verification-loop/scripts/detect-checks.js +102 -0
- package/skills/vite-patterns/SKILL.md +117 -0
- package/skills/vite-patterns/evals/triggers.json +16 -0
- package/skills/vue-patterns/SKILL.md +104 -0
- package/skills/vue-patterns/evals/triggers.json +16 -0
- package/skills/web-accessibility/SKILL.md +119 -0
- package/skills/web-accessibility/evals/triggers.json +16 -0
- package/skills/web-performance/SKILL.md +107 -0
- package/skills/web-performance/evals/triggers.json +16 -0
- package/skills/web-scraping/SKILL.md +80 -0
- package/skills/web-scraping/evals/triggers.json +16 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mtn-momo-integration
|
|
3
|
+
description: Builds and reviews direct integrations with the MTN Mobile Money (MoMo) Open API from momodeveloper.mtn.com, including Collection request-to-pay, Disbursement transfers and refunds, API user and access token handling, X-Reference-Id idempotency, unsigned callbacks confirmed by status checks, and sandbox testing for MTN markets such as Ghana, Uganda and Cameroon. Use when integrating MTN MoMo directly rather than through Paystack or another aggregator, calling requesttopay or disbursement endpoints, handling MoMo callbacks, or debugging MoMo API errors such as 401, 409 or INVALID_CALLBACK_URL_HOST.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Any Agent Skills host. Live calls need network access to sandbox.momodeveloper.mtn.com or the production MoMo API. The sandbox setup script needs Node 18+. Examples are TypeScript; the rules apply to any backend language.
|
|
6
|
+
metadata:
|
|
7
|
+
kete-loom/version: "0.1.0"
|
|
8
|
+
kete-loom/category: integrations
|
|
9
|
+
kete-loom/risk: network
|
|
10
|
+
kete-loom/requires: "node"
|
|
11
|
+
kete-loom/owner: "Kete-org"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# MTN MoMo integration
|
|
15
|
+
|
|
16
|
+
Build a MoMo payment flow that can't be faked, never charges or pays out twice, and still settles when a callback never arrives. This skill is for the MTN MoMo Open API called directly. If the project pays through Paystack, Flutterwave or another aggregator, follow that provider's flow instead.
|
|
17
|
+
|
|
18
|
+
## Ground rules
|
|
19
|
+
|
|
20
|
+
1. **Credentials stay on the server.** The subscription key, API user, API key and access token are all secrets. Read them from the environment, never ship them to a browser or app, and keep one set per product: Collection, Disbursement and Remittance each have their own subscription key.
|
|
21
|
+
2. **One fresh UUID v4 `X-Reference-Id` per attempt, stored before you call.** It is the transaction's ID. MoMo rejects a reused ID with `409 RESOURCE_ALREADY_EXIST`, so a timed-out request can be retried with the **same** ID safely: a 409 means the first attempt was created, so check its status. A new attempt by the customer gets a new ID.
|
|
22
|
+
3. **Callbacks are not signed and are sent once, with no retry.** Never give value because a callback arrived or because of what its body says. Treat it as a hint to run a status check, and poll the status endpoint as well, because some callbacks never arrive.
|
|
23
|
+
4. **Only a status check confirms money moved.** `GET .../requesttopay/{referenceId}` (or `/transfer/{id}`) returns `200` even when the payment failed. Give value only when `status` is `SUCCESSFUL` **and** the amount and currency equal what you stored.
|
|
24
|
+
5. **Fulfilment is idempotent.** Mark the order paid with a guarded update (`UPDATE ... WHERE status = 'pending'`) keyed on the reference, so a callback, a poll and a retry arriving together still fulfil once.
|
|
25
|
+
6. **The server decides the price.** Compute the amount from your own records. `amount` is a string; build it from integer minor units, never from float arithmetic.
|
|
26
|
+
7. **Sandbox first, in EUR.** The sandbox only accepts `EUR` and target environment `sandbox`. Production uses the market's currency and its own target environment (for example `mtnghana` with `GHS`).
|
|
27
|
+
|
|
28
|
+
## Flow (Collection: request to pay)
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
client ──(order id, phone number)──▶ server: create order + payment row with a new UUID (status PENDING)
|
|
32
|
+
server ──POST /collection/v1_0/requesttopay (X-Reference-Id = that UUID)──▶ MoMo: 202 Accepted
|
|
33
|
+
customer approves the prompt on their phone (about 5 minutes before it times out)
|
|
34
|
+
MoMo ──callback (PUT or POST, unsigned)──▶ server: return 200 at once, then run a status check
|
|
35
|
+
server ──GET /collection/v1_0/requesttopay/{id}──▶ SUCCESSFUL + amount/currency match → fulfil once
|
|
36
|
+
background job: poll PENDING payments with backoff until final, or time them out
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The client never learns the result from MoMo directly. It asks your server, which reports the stored status.
|
|
40
|
+
|
|
41
|
+
## Implementation steps
|
|
42
|
+
|
|
43
|
+
1. **Configure** environment variables for the base URL, target environment, currency, and each product's subscription key, API user and API key. In sandbox, create the API user and key with `scripts/sandbox-setup.js` (see `references/sandbox-and-go-live.md`). In production, they come from MTN's partner portal for your market, not from the API.
|
|
44
|
+
2. **Get an access token** per product with `POST /{product}/token/`, using Basic auth (`apiUser:apiKey`), the subscription key and **no body**. Cache it and refresh it before `expires_in` runs out instead of requesting one per payment.
|
|
45
|
+
3. **Request to pay** with the headers and body in `references/api.md`. Validate the phone number first: digits only, with the country code (for example `233...` in Ghana). `payerMessage` and `payeeNote` are at most 160 characters; avoid apostrophes and other special characters, which MoMo rejects with 400.
|
|
46
|
+
4. **Handle callbacks and polling** exactly as `references/callbacks.md` describes: accept both PUT and POST, put a per-transaction token in the callback path, respond 200 fast, and confirm every result with a status check.
|
|
47
|
+
5. **Map failures** to clear messages using the reason codes in `references/api.md`: unknown number, not enough funds, approval rejected, timed out (`EXPIRED`, `COULD_NOT_PERFORM_TRANSACTION`), limit reached. Let the customer retry with a new reference.
|
|
48
|
+
6. **Payouts and refunds** (Disbursement) follow the same rules: a new UUID per transfer, store it first, confirm by status. Production Disbursement also requires your server's IP to be whitelisted, otherwise calls fail with `403`.
|
|
49
|
+
7. **Persist** for each payment: the reference UUID, order id, expected amount and currency, phone number, status, `financialTransactionId`, failure reason, and timestamps. Reconcile against MoMo's status regularly.
|
|
50
|
+
|
|
51
|
+
## Reviewing an existing integration
|
|
52
|
+
|
|
53
|
+
Check each ground rule, then look for these common bugs:
|
|
54
|
+
|
|
55
|
+
- Fulfilment triggered by the callback body, or by the 202 from request to pay.
|
|
56
|
+
- A new `X-Reference-Id` generated on retry, which can charge the customer twice, or the same ID reused for a new attempt, which fails with 409.
|
|
57
|
+
- No polling fallback, so payments stay PENDING forever when a callback is lost.
|
|
58
|
+
- Status treated as success because the HTTP response was 200.
|
|
59
|
+
- A token requested for every call, or a Collection key used against Disbursement (401).
|
|
60
|
+
- Callback host that differs from the API user's `providerCallbackHost`, uses plain HTTP, or uses an IP address (`INVALID_CALLBACK_URL_HOST`).
|
|
61
|
+
- Sandbox values (`EUR`, `sandbox`) left in production configuration, or production credentials used in the sandbox.
|
|
62
|
+
- Amounts built with floats, or a price taken from the client.
|
|
63
|
+
|
|
64
|
+
## Before you finish
|
|
65
|
+
|
|
66
|
+
The API reference at momodeveloper.mtn.com is the final word on field names and behaviour; this skill gives the rules and the shape. Production base URLs and market currencies vary by country, so confirm them with MTN for the market you go live in. Then test in the sandbox with the test numbers in `references/sandbox-and-go-live.md`: a successful payment, a failure, a rejection, an expired approval, a callback that never arrives (polling must settle it), a duplicate callback, and a retried request after a simulated timeout (must not charge twice).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"should_trigger": [
|
|
3
|
+
"Integrate the MTN MoMo Open API so customers in Ghana can pay with request to pay",
|
|
4
|
+
"Our momodeveloper sandbox keeps returning 401 when I call /collection/token/",
|
|
5
|
+
"MoMo callbacks never reach our server and payments stay PENDING forever",
|
|
6
|
+
"Add payouts to agents' mobile money wallets using the MTN MoMo disbursement API",
|
|
7
|
+
"Why does requesttopay return 409 when we retry after a timeout?"
|
|
8
|
+
],
|
|
9
|
+
"should_not_trigger": [
|
|
10
|
+
"Let users pay with MTN MoMo through Paystack in the mobile app",
|
|
11
|
+
"My Paystack webhook says invalid signature every time, can you fix it?",
|
|
12
|
+
"Integrate M-Pesa STK push with the Safaricom Daraja API",
|
|
13
|
+
"Write a market report on mobile money adoption in Uganda",
|
|
14
|
+
"Add Stripe checkout to the pricing page"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# MTN MoMo Open API reference
|
|
2
|
+
|
|
3
|
+
Read this when writing or reviewing the HTTP calls. Checked against the API reference on momodeveloper.mtn.com in October 2026; confirm against the current reference before shipping.
|
|
4
|
+
|
|
5
|
+
## Base URLs and target environments
|
|
6
|
+
|
|
7
|
+
| Environment | Base URL | `X-Target-Environment` |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Sandbox | `https://sandbox.momodeveloper.mtn.com` | `sandbox` (currency must be `EUR`) |
|
|
10
|
+
| Production | `https://proxy.momoapi.mtn.com` (stated by MTN on its developer community forum, not in the API reference; confirm with MTN for your market) | the market value below |
|
|
11
|
+
|
|
12
|
+
Paths are prefixed by product: `/collection`, `/disbursement`, `/remittance`. Sandbox provisioning (`/v1_0/apiuser`) has no product prefix.
|
|
13
|
+
|
|
14
|
+
Production target environments documented by MTN: `mtnuganda`, `mtnghana`, `mtnivorycoast`, `mtnzambia`, `mtncameroon`, `mtnbenin`, `mtncongo`, `mtnswaziland`, `mtnguineaconakry`, `mtnsouthafrica`, `mtnliberia`, `mtnsouthsudan`, `mtnnigeria`, `mtnrwanda`. A wrong value fails with `NOT_ALLOWED_TARGET_ENVIRONMENT`.
|
|
15
|
+
|
|
16
|
+
Currencies by market, from MTN's community forum (confirm before going live): Uganda `UGX`, Ghana `GHS`, Côte d'Ivoire `XOF`, Zambia `ZMW`, Cameroon `XAF`, Benin `XOF`, Congo `XAF`, Eswatini `SZL`, Guinea `GNF`, South Africa `ZAR`, Liberia `LRD`.
|
|
17
|
+
|
|
18
|
+
## Authentication
|
|
19
|
+
|
|
20
|
+
- **Subscription key**: header `Ocp-Apim-Subscription-Key`. Each product has its own primary and secondary key; either works. A key from the wrong product gives 401.
|
|
21
|
+
- **API user and API key**: sandbox users are created with the API (see `sandbox-and-go-live.md`). Production users are created in the partner portal for your market. Sandbox users only work in the sandbox.
|
|
22
|
+
- **Access token**: `POST /{product}/token/` with `Authorization: Basic base64(apiUser:apiKey)`, the subscription key, and **no body** (a body gives 400). The response is `{ "access_token": "...", "token_type": "...", "expires_in": <seconds> }`. Send it as `Authorization: Bearer <access_token>`, cache it per product, and refresh it shortly before `expires_in` ends.
|
|
23
|
+
- `bc-authorize` and `/oauth2/token/` exist for consent-based calls such as `/oauth2/v1_0/userinfo`. Request to pay, transfers, balance and basic user info use the plain token above.
|
|
24
|
+
|
|
25
|
+
## Request to pay (Collection)
|
|
26
|
+
|
|
27
|
+
`POST /collection/v1_0/requesttopay`
|
|
28
|
+
|
|
29
|
+
| Header | Value |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `Authorization` | `Bearer <access_token>` |
|
|
32
|
+
| `X-Reference-Id` | a new UUID v4 you stored first; it becomes the transaction ID |
|
|
33
|
+
| `X-Target-Environment` | `sandbox` or the market value |
|
|
34
|
+
| `Ocp-Apim-Subscription-Key` | the Collection subscription key |
|
|
35
|
+
| `X-Callback-Url` | optional; HTTPS on the API user's callback host |
|
|
36
|
+
| `Content-Type` | `application/json` |
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"amount": "25.50",
|
|
41
|
+
"currency": "GHS",
|
|
42
|
+
"externalId": "order_8f2c1a",
|
|
43
|
+
"payer": { "partyIdType": "MSISDN", "partyId": "233241234567" },
|
|
44
|
+
"payerMessage": "Order 8f2c1a",
|
|
45
|
+
"payeeNote": "Order 8f2c1a"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `amount` is a string in the currency's major unit. Build it from integer minor units with `formatAmount` below, which knows that UGX, XOF, XAF and GNF have no decimals. MTN doesn't document whether every market accepts decimals, so confirm the format with a small real payment before going live.
|
|
50
|
+
- `partyIdType` is `MSISDN`, `EMAIL` or `PARTY_CODE`. An MSISDN includes the country code; MTN's examples use digits only, without `+`.
|
|
51
|
+
- `externalId` is your reference for reconciliation; it doesn't need to be unique.
|
|
52
|
+
- `payerMessage` and `payeeNote`: at most 160 characters, no apostrophes or special characters.
|
|
53
|
+
|
|
54
|
+
Responses: `202 Accepted` with no body (the payment is now PENDING). `409 RESOURCE_ALREADY_EXIST` if the `X-Reference-Id` was used before. `400` for a bad body, `401` for a bad key or token, `500` with a reason code for environment, currency or callback-host errors.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { randomUUID } from 'node:crypto';
|
|
58
|
+
|
|
59
|
+
const MOMO = process.env.MOMO_BASE_URL!; // https://sandbox.momodeveloper.mtn.com
|
|
60
|
+
const ENV = process.env.MOMO_TARGET_ENVIRONMENT!; // sandbox | mtnghana | ...
|
|
61
|
+
const KEY = process.env.MOMO_COLLECTION_SUBSCRIPTION_KEY!;
|
|
62
|
+
|
|
63
|
+
export async function requestToPay(order: { id: string; amountMinor: number; currency: string; msisdn: string }) {
|
|
64
|
+
const referenceId = randomUUID();
|
|
65
|
+
await db.payments.insert({ referenceId, orderId: order.id, amountMinor: order.amountMinor, currency: order.currency, status: 'PENDING' });
|
|
66
|
+
const res = await fetch(`${MOMO}/collection/v1_0/requesttopay`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
authorization: `Bearer ${await collectionToken()}`,
|
|
70
|
+
'x-reference-id': referenceId,
|
|
71
|
+
'x-target-environment': ENV,
|
|
72
|
+
'ocp-apim-subscription-key': KEY,
|
|
73
|
+
'x-callback-url': callbackUrl(referenceId),
|
|
74
|
+
'content-type': 'application/json',
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
amount: formatAmount(order.amountMinor, order.currency),
|
|
78
|
+
currency: order.currency,
|
|
79
|
+
externalId: order.id,
|
|
80
|
+
payer: { partyIdType: 'MSISDN', partyId: order.msisdn },
|
|
81
|
+
payerMessage: `Order ${order.id}`,
|
|
82
|
+
payeeNote: `Order ${order.id}`,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
// 409 on a retry with the same referenceId means the first attempt was created: check its status instead.
|
|
86
|
+
if (res.status !== 202 && res.status !== 409) throw new Error(`requesttopay failed: ${res.status} ${await res.text()}`);
|
|
87
|
+
return referenceId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Money as integer minor units, converted with integer maths only. Intl knows each currency's ISO decimals (GHS 2, UGX 0).
|
|
91
|
+
const decimals = (currency: string) => new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions().maximumFractionDigits ?? 2;
|
|
92
|
+
|
|
93
|
+
export function formatAmount(minor: number, currency: string): string {
|
|
94
|
+
const d = decimals(currency);
|
|
95
|
+
if (d === 0) return String(minor);
|
|
96
|
+
const s = String(minor).padStart(d + 1, '0');
|
|
97
|
+
return `${s.slice(0, -d)}.${s.slice(-d)}`; // 2550 GHS -> "25.50"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// MoMo may echo "25.5" or "25.50": compare in minor units, not as strings.
|
|
101
|
+
export function toMinor(amount: string, currency: string): number {
|
|
102
|
+
const d = decimals(currency);
|
|
103
|
+
const [whole, frac = ''] = amount.split('.');
|
|
104
|
+
if (!/^\d+$/.test(whole) || !/^\d*$/.test(frac) || frac.length > d) return NaN;
|
|
105
|
+
return Number(whole) * 10 ** d + Number(frac.padEnd(d, '0') || 0);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If the request times out or returns 5xx, retry it with the **same** `referenceId` and headers. Never generate a new one for the same attempt.
|
|
110
|
+
|
|
111
|
+
## Status
|
|
112
|
+
|
|
113
|
+
`GET /collection/v1_0/requesttopay/{referenceId}` with the same auth headers (no callback header). Returns `200` whatever the outcome:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"amount": "25.50",
|
|
118
|
+
"currency": "GHS",
|
|
119
|
+
"financialTransactionId": "1234567890",
|
|
120
|
+
"externalId": "order_8f2c1a",
|
|
121
|
+
"payer": { "partyIdType": "MSISDN", "partyId": "233241234567" },
|
|
122
|
+
"status": "FAILED",
|
|
123
|
+
"reason": { "code": "NOT_ENOUGH_FUNDS", "message": "..." }
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`status` is `PENDING`, `SUCCESSFUL` or `FAILED`. An unknown reference gives `404 RESOURCE_NOT_FOUND`.
|
|
128
|
+
|
|
129
|
+
## Reason codes
|
|
130
|
+
|
|
131
|
+
| Code | Tell the customer |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `PAYER_NOT_FOUND` | This number isn't registered for MoMo |
|
|
134
|
+
| `NOT_ENOUGH_FUNDS` (also often `INTERNAL_PROCESSING_ERROR`) | Not enough money in the wallet |
|
|
135
|
+
| `PAYER_LIMIT_REACHED` | The wallet's limit was reached |
|
|
136
|
+
| `APPROVAL_REJECTED`, `PAYMENT_NOT_APPROVED`, `TRANSACTION_CANCELED` | The payment was declined on the phone |
|
|
137
|
+
| `EXPIRED`, `COULD_NOT_PERFORM_TRANSACTION` | The approval timed out (about 5 minutes) |
|
|
138
|
+
| `PAYEE_NOT_ALLOWED_TO_RECEIVE`, `NOT_ALLOWED` | Merchant-side problem: log it and alert, don't blame the customer |
|
|
139
|
+
| `NOT_ALLOWED_TARGET_ENVIRONMENT`, `INVALID_CURRENCY`, `INVALID_CALLBACK_URL_HOST` | Configuration error on your side |
|
|
140
|
+
| `SERVICE_UNAVAILABLE` | Try again later |
|
|
141
|
+
|
|
142
|
+
## Disbursement and refunds
|
|
143
|
+
|
|
144
|
+
- **Transfer (payout)**: `POST /disbursement/v1_0/transfer`, same headers as request to pay with the Disbursement key and token. The body uses `payee` instead of `payer`. `202` or `409`. Status: `GET /disbursement/v1_0/transfer/{referenceId}`.
|
|
145
|
+
- **Refund**: `POST /disbursement/v1_0/refund` (or `/v2_0/refund`) with `amount`, `currency`, `externalId`, `payerMessage`, `payeeNote` and `referenceIdToRefund` (the request-to-pay UUID). Status: `GET /disbursement/v1_0/refund/{referenceId}`. The v1 and v2 endpoints take the same body; MTN documents v1 callbacks as POST and v2 as PUT.
|
|
146
|
+
- **Deposit**: `/disbursement/v1_0/deposit` and `/v2_0/deposit`, same body shape as a transfer.
|
|
147
|
+
- Production Disbursement calls need your server's IP whitelisted with MTN, or they fail with `403`.
|
|
148
|
+
|
|
149
|
+
## Other endpoints
|
|
150
|
+
|
|
151
|
+
- `GET /{product}/v1_0/account/balance` returns `{ "availableBalance": "...", "currency": "..." }`.
|
|
152
|
+
- `GET /{product}/v1_0/accountholder/msisdn/{msisdn}/active` returns `{ "result": true }`. The type is lowercase. It shows the account exists and is active, not that it can pay a given amount.
|
|
153
|
+
- `GET /{product}/v1_0/accountholder/msisdn/{msisdn}/basicuserinfo` returns the holder's name. Useful to show "Pay Ama Mensah?" before a payout.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Callbacks and status polling
|
|
2
|
+
|
|
3
|
+
Read this when building or reviewing the callback endpoint or the code that settles PENDING payments.
|
|
4
|
+
|
|
5
|
+
## What MoMo guarantees, and what it doesn't
|
|
6
|
+
|
|
7
|
+
- Callbacks are **not signed** and carry no authentication. Anyone who learns the URL can call it.
|
|
8
|
+
- MoMo sends each callback **once, with no retry**. If your server is down or slow, it is lost.
|
|
9
|
+
- The documentation is inconsistent about the method (PUT for most, POST for v1 deposit and refund), so **accept both**.
|
|
10
|
+
- The body looks like the status response, but you must not trust it.
|
|
11
|
+
- The callback URL must be **HTTPS on port 443**, on the host registered as the API user's `providerCallbackHost` (a hostname, not an IP address), or MoMo rejects the request with `INVALID_CALLBACK_URL_HOST`. The certificate must chain to a common public CA (for example Let's Encrypt, DigiCert, Sectigo, Google or Amazon), or delivery can fail.
|
|
12
|
+
- Only path parameters are allowed in the callback URL, not query strings.
|
|
13
|
+
|
|
14
|
+
So a callback only tells you "check this reference now". The status check decides.
|
|
15
|
+
|
|
16
|
+
## Callback URL with a per-transaction token
|
|
17
|
+
|
|
18
|
+
Make each callback URL unguessable by putting a token derived from the reference in the path:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import crypto from 'node:crypto';
|
|
22
|
+
|
|
23
|
+
const SECRET = process.env.MOMO_CALLBACK_SECRET!; // random, server-side only
|
|
24
|
+
const tokenFor = (referenceId: string) => crypto.createHmac('sha256', SECRET).update(referenceId).digest('hex');
|
|
25
|
+
|
|
26
|
+
export const callbackUrl = (referenceId: string) =>
|
|
27
|
+
`https://pay.example.com/momo/callback/${referenceId}/${tokenFor(referenceId)}`;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Callback handler (Express)
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
app.all('/momo/callback/:referenceId/:token', (req, res) => {
|
|
34
|
+
if (req.method !== 'PUT' && req.method !== 'POST') return res.sendStatus(405);
|
|
35
|
+
const { referenceId, token } = req.params;
|
|
36
|
+
const expected = tokenFor(referenceId);
|
|
37
|
+
const valid = token.length === expected.length && crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expected));
|
|
38
|
+
if (!valid) return res.sendStatus(404);
|
|
39
|
+
|
|
40
|
+
res.sendStatus(200); // respond first; MoMo won't retry, but it shouldn't wait on you either
|
|
41
|
+
queue.add('momo-settle', { referenceId }); // settle from a status check, never from req.body
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Settling a payment
|
|
46
|
+
|
|
47
|
+
One function settles a payment, and both the callback job and the poller call it:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
export async function settle(referenceId: string) {
|
|
51
|
+
const payment = await db.payments.findByReference(referenceId);
|
|
52
|
+
if (!payment || payment.status !== 'PENDING') return; // unknown, or already final
|
|
53
|
+
|
|
54
|
+
const res = await fetch(`${MOMO}/collection/v1_0/requesttopay/${referenceId}`, {
|
|
55
|
+
headers: {
|
|
56
|
+
authorization: `Bearer ${await collectionToken()}`,
|
|
57
|
+
'x-target-environment': ENV,
|
|
58
|
+
'ocp-apim-subscription-key': KEY,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) throw new Error(`status check failed: ${res.status}`); // the poller will retry
|
|
62
|
+
const s = await res.json();
|
|
63
|
+
|
|
64
|
+
if (s.status === 'PENDING') return;
|
|
65
|
+
const matches = s.currency === payment.currency && toMinor(s.amount, s.currency) === payment.amountMinor;
|
|
66
|
+
if (s.status === 'SUCCESSFUL' && matches) {
|
|
67
|
+
// Guarded update: only one caller wins, so fulfilment happens once.
|
|
68
|
+
const won = await db.payments.update({ referenceId, status: 'PENDING' }, { status: 'SUCCESSFUL', financialTransactionId: s.financialTransactionId });
|
|
69
|
+
if (won) await fulfilOrder(payment.orderId);
|
|
70
|
+
} else {
|
|
71
|
+
await db.payments.update({ referenceId, status: 'PENDING' }, { status: 'FAILED', reason: s.reason?.code ?? (matches ? 'UNKNOWN' : 'AMOUNT_MISMATCH') });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
An amount or currency mismatch on a `SUCCESSFUL` payment is not a normal failure: flag it for a person to review rather than refunding or fulfilling automatically.
|
|
77
|
+
|
|
78
|
+
## Polling fallback
|
|
79
|
+
|
|
80
|
+
Run a background job for payments still PENDING:
|
|
81
|
+
|
|
82
|
+
- First check about 10 seconds after the 202, then back off (for example 10s, 20s, 40s, 60s, then every 60s).
|
|
83
|
+
- The customer has about 5 minutes to approve. After about 10 minutes, keep checking at a slower rate (every 15 minutes) for a few hours rather than marking the payment failed straight away, because MoMo may still settle it.
|
|
84
|
+
- Never create a new request to pay for the same attempt while one is PENDING.
|
|
85
|
+
- The client's "Check status" button should call `settle` (rate-limited), not MoMo directly.
|
|
86
|
+
|
|
87
|
+
## Tests
|
|
88
|
+
|
|
89
|
+
- A callback with a wrong token returns 404 and settles nothing.
|
|
90
|
+
- A callback whose body says SUCCESSFUL while the status check says FAILED results in FAILED.
|
|
91
|
+
- The same callback delivered twice, plus a poll at the same moment, fulfils once.
|
|
92
|
+
- No callback at all: the poller settles the payment.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Sandbox setup, test numbers and going live
|
|
2
|
+
|
|
3
|
+
Read this when setting up a sandbox, writing tests, or preparing for production.
|
|
4
|
+
|
|
5
|
+
## Create a sandbox API user and key
|
|
6
|
+
|
|
7
|
+
1. Sign up at momodeveloper.mtn.com and subscribe to the product you need (Collection, Disbursement or Remittance). Copy its **primary key** from your profile.
|
|
8
|
+
2. Run the setup script (`<skill-dir>` is the folder holding this skill's SKILL.md), with the subscription key in the environment and your callback host (the hostname that will receive callbacks; it must be a hostname you control, served over HTTPS):
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
MOMO_SUBSCRIPTION_KEY=... node <skill-dir>/scripts/sandbox-setup.js --callback-host pay.example.com
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
It creates an API user (`POST /v1_0/apiuser` with a new UUID), generates its API key (`POST /v1_0/apiuser/{id}/apikey`), checks that the pair can get a Collection token, and prints `MOMO_API_USER` and `MOMO_API_KEY` lines for your `.env`. Pass `--product disbursement` or `--product remittance` to check the token against another product, using that product's subscription key.
|
|
15
|
+
3. Store the API key immediately. MoMo can't show it again; if it's lost, run the script again for a new user.
|
|
16
|
+
|
|
17
|
+
The sandbox only accepts target environment `sandbox` and currency `EUR`.
|
|
18
|
+
|
|
19
|
+
## Sandbox test numbers
|
|
20
|
+
|
|
21
|
+
For request to pay (and deposit and request to withdraw), the payer's MSISDN decides the result. Any other number succeeds.
|
|
22
|
+
|
|
23
|
+
| MSISDN | Result |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `46733123450` | Failed |
|
|
26
|
+
| `46733123451` | Rejected |
|
|
27
|
+
| `46733123452` | Expired |
|
|
28
|
+
| `46733123453` | Ongoing (stays PENDING) |
|
|
29
|
+
| `46733123454` | Delayed |
|
|
30
|
+
| `46733123455` | Not found |
|
|
31
|
+
| `46733123456` | Payee not allowed to receive |
|
|
32
|
+
| `46733123457` | Not allowed |
|
|
33
|
+
| `46733123458` | Not allowed target environment |
|
|
34
|
+
| `46733123459` | Invalid callback URL host |
|
|
35
|
+
| `46733123460` | Invalid currency |
|
|
36
|
+
| `46733123461` | Internal processing error |
|
|
37
|
+
| `46733123462` | Service unavailable |
|
|
38
|
+
| `46733123463` | Could not perform transaction |
|
|
39
|
+
|
|
40
|
+
For the account-holder `active` check, `46733123450` is not found, `…451` not active, `…452` not allowed, `…453` not allowed target environment, `…454` internal processing error and `…455` service unavailable. Refund outcomes are selected by the refund amount (1 to 13); see MTN's sandbox use cases page.
|
|
41
|
+
|
|
42
|
+
Use `46733123453` (Ongoing) and a stopped callback endpoint to test that polling settles a payment.
|
|
43
|
+
|
|
44
|
+
## Going live
|
|
45
|
+
|
|
46
|
+
- Complete MTN's go-live process for your market. Production API users and keys are created in that market's partner portal; sandbox users don't work there.
|
|
47
|
+
- Switch the base URL, target environment (for example `mtnghana`) and currency (for example `GHS`) together, from configuration, not code.
|
|
48
|
+
- Register your production callback host in the partner portal and make sure the certificate chains to a common public CA.
|
|
49
|
+
- For Disbursement, get your server's outbound IP addresses whitelisted, or payouts fail with `403`.
|
|
50
|
+
- Confirm the amount format your market accepts (whole units or decimals) with a small real payment.
|
|
51
|
+
- Keep sandbox and production credentials in separate secret stores so one can't be used for the other.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Creates an MTN MoMo sandbox API user and API key, then checks they can get an access token.
|
|
5
|
+
// Sandbox only: production API users come from MTN's partner portal.
|
|
6
|
+
//
|
|
7
|
+
// MOMO_SUBSCRIPTION_KEY=<product primary key> node sandbox-setup.js --callback-host pay.example.com [--product collection]
|
|
8
|
+
//
|
|
9
|
+
// Prints MOMO_API_USER / MOMO_API_KEY lines for your .env. The subscription key is never printed.
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
|
|
13
|
+
const SANDBOX = 'https://sandbox.momodeveloper.mtn.com';
|
|
14
|
+
const PRODUCTS = ['collection', 'disbursement', 'remittance'];
|
|
15
|
+
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const opts = { product: 'collection' };
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
if (argv[i] === '--callback-host') opts.callbackHost = argv[++i];
|
|
20
|
+
else if (argv[i] === '--product') opts.product = argv[++i];
|
|
21
|
+
else throw new Error(`unknown argument ${argv[i]}`);
|
|
22
|
+
}
|
|
23
|
+
if (!opts.callbackHost) throw new Error('--callback-host is required (a hostname such as pay.example.com)');
|
|
24
|
+
// MoMo matches callback URLs against this host, and rejects IPs: a bare hostname only.
|
|
25
|
+
if (!/^(?=.{1,253}$)([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i.test(opts.callbackHost)) {
|
|
26
|
+
throw new Error(`--callback-host must be a bare hostname like pay.example.com, not "${opts.callbackHost}"`);
|
|
27
|
+
}
|
|
28
|
+
if (!PRODUCTS.includes(opts.product)) throw new Error(`--product must be one of ${PRODUCTS.join(', ')}`);
|
|
29
|
+
return opts;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function call(fetchImpl, url, init, expected) {
|
|
33
|
+
const res = await fetchImpl(url, init);
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
if (res.status !== expected) throw new Error(`${init.method} ${url.replace(SANDBOX, '')} returned ${res.status}: ${text.slice(0, 300) || '(empty body)'}`);
|
|
36
|
+
return text ? JSON.parse(text) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function setup({ subscriptionKey, callbackHost, product = 'collection', fetchImpl = fetch, uuid = crypto.randomUUID }) {
|
|
40
|
+
if (!subscriptionKey) throw new Error('set MOMO_SUBSCRIPTION_KEY to the primary key of the product you subscribed to');
|
|
41
|
+
const apiUser = uuid();
|
|
42
|
+
const key = { 'ocp-apim-subscription-key': subscriptionKey };
|
|
43
|
+
|
|
44
|
+
await call(fetchImpl, `${SANDBOX}/v1_0/apiuser`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { ...key, 'x-reference-id': apiUser, 'content-type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({ providerCallbackHost: callbackHost }),
|
|
48
|
+
}, 201);
|
|
49
|
+
|
|
50
|
+
const { apiKey } = await call(fetchImpl, `${SANDBOX}/v1_0/apiuser/${apiUser}/apikey`, { method: 'POST', headers: key }, 201);
|
|
51
|
+
|
|
52
|
+
// No body on the token call: MoMo rejects one with 400.
|
|
53
|
+
const basic = Buffer.from(`${apiUser}:${apiKey}`).toString('base64');
|
|
54
|
+
const token = await call(fetchImpl, `${SANDBOX}/${product}/token/`, { method: 'POST', headers: { ...key, authorization: `Basic ${basic}` } }, 200);
|
|
55
|
+
if (!token || !token.access_token) throw new Error('token call succeeded but returned no access_token');
|
|
56
|
+
|
|
57
|
+
return { apiUser, apiKey, expiresIn: token.expires_in };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main(argv, env = process.env) {
|
|
61
|
+
try {
|
|
62
|
+
const opts = parseArgs(argv);
|
|
63
|
+
const r = await setup({ subscriptionKey: env.MOMO_SUBSCRIPTION_KEY, ...opts });
|
|
64
|
+
console.log(`# MoMo sandbox API user for ${opts.callbackHost} (token check against ${opts.product} passed${r.expiresIn ? `, expires_in ${r.expiresIn}s` : ''})`);
|
|
65
|
+
console.log('# Store these now: MoMo cannot show this API key again.');
|
|
66
|
+
console.log(`MOMO_BASE_URL=${SANDBOX}`);
|
|
67
|
+
console.log('MOMO_TARGET_ENVIRONMENT=sandbox');
|
|
68
|
+
console.log(`MOMO_API_USER=${r.apiUser}`);
|
|
69
|
+
console.log(`MOMO_API_KEY=${r.apiKey}`);
|
|
70
|
+
return 0;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error(`sandbox-setup: ${e.message}`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (require.main === module) main(process.argv.slice(2)).then((code) => (process.exitCode = code));
|
|
78
|
+
|
|
79
|
+
module.exports = { parseArgs, setup, main };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mysql-patterns
|
|
3
|
+
description: MySQL and MariaDB on InnoDB, covering clustered primary keys and secondary index design, reading EXPLAIN and EXPLAIN ANALYZE output, utf8mb4 character sets and collations, REPEATABLE READ with gap and next-key locks, deadlock diagnosis, online DDL algorithms, and stale reads from lagging replicas. Use when creating or tuning MySQL or MariaDB tables, a MySQL query is slow, emoji or accented text is mangled, InnoDB lock waits or deadlocks appear, or reads from a replica return outdated data.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Any Agent Skills host. Running queries needs the mysql or mariadb client and a reachable server; never point it at production without the user's say-so.
|
|
6
|
+
metadata:
|
|
7
|
+
kete-loom/version: "0.1.0"
|
|
8
|
+
kete-loom/category: data
|
|
9
|
+
kete-loom/risk: runs-commands
|
|
10
|
+
kete-loom/requires: "mysql"
|
|
11
|
+
kete-loom/owner: "Kete-org"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# MySQL and MariaDB patterns
|
|
15
|
+
|
|
16
|
+
InnoDB stores each table as a btree ordered by its primary key, so key choice drives everything else. Design for that, check every query with `EXPLAIN`, and assume replicas are behind. For step-by-step live schema changes use the `database-migrations` skill; for PostgreSQL use `postgres-patterns`.
|
|
17
|
+
|
|
18
|
+
## 0. Before you start
|
|
19
|
+
|
|
20
|
+
1. Identify the server: `SELECT VERSION();`. MySQL and MariaDB have diverged (optimizer, JSON, defaults, replication syntax). Check that product's docs for its version before giving version-specific advice.
|
|
21
|
+
2. Check the engine: `SHOW TABLE STATUS WHERE Name = 't';`. Everything here assumes InnoDB. If you find MyISAM, tell the user: it has no transactions or row locks.
|
|
22
|
+
3. Ask which environment you may query. `EXPLAIN ANALYZE` **executes** the statement.
|
|
23
|
+
|
|
24
|
+
## 1. Schema rules for InnoDB
|
|
25
|
+
|
|
26
|
+
- Primary key: short and monotonic, usually `BIGINT UNSIGNED AUTO_INCREMENT`. Every secondary index stores the primary key, so a wide PK makes every index wide. Random UUID v4 PKs scatter inserts across pages; if you need UUIDs, store them as `BINARY(16)` and prefer a time-ordered variant.
|
|
27
|
+
- Always declare a primary key. Without one InnoDB invents a hidden one you can't use, and row-based replication of updates gets slow.
|
|
28
|
+
- Money: `DECIMAL(p, s)` or integer minor units, never `FLOAT`/`DOUBLE`.
|
|
29
|
+
- Time: `DATETIME` for business dates you store in UTC; `TIMESTAMP` converts through the session time zone and has a 2038 upper limit. Pick one convention and set the connection time zone explicitly.
|
|
30
|
+
- Booleans are `TINYINT(1)`; enumerations a lookup table or `ENUM` only if the set truly never changes.
|
|
31
|
+
- Turn on strict mode (`sql_mode` containing `STRICT_TRANS_TABLES`), so bad data errors instead of being silently truncated. Check the server's current value before assuming.
|
|
32
|
+
- Declare foreign keys: InnoDB creates an index on the FK column if none exists, and enforces the reference.
|
|
33
|
+
|
|
34
|
+
## 2. Character sets and collations
|
|
35
|
+
|
|
36
|
+
1. Use `utf8mb4` everywhere: server, database, table, column **and** connection. `utf8`/`utf8mb3` stores at most 3 bytes per character, so emoji and some CJK characters fail or turn into `?`.
|
|
37
|
+
2. Check all layers:
|
|
38
|
+
```sql
|
|
39
|
+
SHOW VARIABLES LIKE 'character_set_%';
|
|
40
|
+
SHOW VARIABLES LIKE 'collation_%';
|
|
41
|
+
SHOW CREATE TABLE t;
|
|
42
|
+
```
|
|
43
|
+
The driver must also connect with `utf8mb4` (connection option `charset=utf8mb4` or the driver's equivalent).
|
|
44
|
+
3. Collation decides equality and sorting: `_ci` is case-insensitive, `_as`/`_ai` accent sensitive or insensitive, `_bin` byte-exact. A unique index on a `_ci` column rejects `Bob` when `bob` exists. Use a `_bin` or `_cs` collation where case matters (tokens, codes).
|
|
45
|
+
4. Joining columns with different charsets or collations disables index use on one side and can raise `Illegal mix of collations`. Make them match.
|
|
46
|
+
5. Converting a table (`ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 COLLATE ...`) rebuilds it and can lengthen index keys. Plan it as a migration, not a quick fix.
|
|
47
|
+
|
|
48
|
+
## 3. Index design
|
|
49
|
+
|
|
50
|
+
1. Composite index order: equality columns first, then one range or sort column. `(tenant_id, status, created_at)` serves `WHERE tenant_id = ? AND status = ? ORDER BY created_at`.
|
|
51
|
+
2. Leftmost prefix rule: an index on `(a, b, c)` helps queries on `a`, `a, b` and `a, b, c`, not `b` alone.
|
|
52
|
+
3. A covering index (all selected columns in the index) shows `Using index` in `EXPLAIN` and skips the table lookup.
|
|
53
|
+
4. Don't index low-cardinality columns alone (`is_active`). Put them inside a composite after a selective column.
|
|
54
|
+
5. Long strings: prefix indexes (`col(32)`) save space but cannot cover a query or serve `ORDER BY` fully.
|
|
55
|
+
6. Remove duplicates: `(a)` is redundant when `(a, b)` exists. `sys.schema_redundant_indexes` and `sys.schema_unused_indexes` list candidates on MySQL; confirm across all replicas before dropping.
|
|
56
|
+
|
|
57
|
+
## 4. Read a plan
|
|
58
|
+
|
|
59
|
+
Run `EXPLAIN <query>` (or `EXPLAIN FORMAT=TREE`, and `EXPLAIN ANALYZE` on servers that support it, on a copy). Key columns and red flags: `references/explain.md`.
|
|
60
|
+
|
|
61
|
+
Quick rules: `type = ALL` on a big table, `rows` far above the result size, `Using filesort` or `Using temporary` on a hot query, and `key = NULL` when you expected an index all need a fix. Common causes: a function or implicit cast on the indexed column, a leading `%` in `LIKE`, a charset mismatch in a join, or an `OR` across different columns.
|
|
62
|
+
|
|
63
|
+
After any change, re-run the same `EXPLAIN` and the query itself, and report both.
|
|
64
|
+
|
|
65
|
+
## 5. Transactions and locking
|
|
66
|
+
|
|
67
|
+
- Default isolation is `REPEATABLE READ`. Plain `SELECT` reads a consistent snapshot; locking reads (`SELECT ... FOR UPDATE`, `FOR SHARE`), `UPDATE` and `DELETE` see the latest committed rows and take **next-key locks** (row plus the gap before it).
|
|
68
|
+
- An `UPDATE` or `DELETE` whose `WHERE` isn't served by an index locks every row it scans, which can be the whole table. Index the predicate of every write statement.
|
|
69
|
+
- Gap locks make "check if exists, then insert" deadlock under load. Use a unique key with `INSERT ... ON DUPLICATE KEY UPDATE`, or `INSERT IGNORE` only when silently dropping the row is truly fine.
|
|
70
|
+
- Job queues: `SELECT ... FOR UPDATE SKIP LOCKED` (MySQL 8.0+, MariaDB 10.6+).
|
|
71
|
+
- Deadlocks are normal under concurrency: the app must catch error 1213 and retry the transaction. Lock wait timeouts are error 1205 (`innodb_lock_wait_timeout`).
|
|
72
|
+
- Diagnose with `SHOW ENGINE INNODB STATUS\G` (section `LATEST DETECTED DEADLOCK`) and, on MySQL 8, `performance_schema.data_locks` and `sys.innodb_lock_waits`.
|
|
73
|
+
- Keep transactions short. Autocommit off in a driver plus a forgotten commit holds locks and undo history indefinitely.
|
|
74
|
+
|
|
75
|
+
## 6. Replication lag
|
|
76
|
+
|
|
77
|
+
- Replicas apply changes after the source commits. Lag can be seconds or more during big writes or DDL.
|
|
78
|
+
- Read-your-writes: after a user writes, read their data from the source (for a short window, or per session), not a replica.
|
|
79
|
+
- Never use a replica read to decide a write (balance checks, uniqueness, "already processed?").
|
|
80
|
+
- One huge `UPDATE`/`DELETE` replicates as one huge event and stalls replicas. Batch it (see `database-migrations`).
|
|
81
|
+
- Measure lag with `SHOW REPLICA STATUS\G` (`SHOW SLAVE STATUS` on older servers) or a heartbeat table such as `pt-heartbeat`; `Seconds_Behind_Source` (`Seconds_Behind_Master` on older servers and MariaDB) is `NULL` when a replication thread is stopped and only measures against the relay log, so it can read 0 while the I/O thread is behind.
|
|
82
|
+
|
|
83
|
+
## 7. Online schema changes
|
|
84
|
+
|
|
85
|
+
Prefer `ALTER TABLE ..., ALGORITHM=INSTANT` or `ALGORITHM=INPLACE, LOCK=NONE` and let the server refuse if it can't. Stating the algorithm makes the statement fail instead of quietly copying the table. Details, plus when to reach for `gh-ost` or `pt-online-schema-change`: `references/online-ddl.md`.
|
|
86
|
+
|
|
87
|
+
## Never
|
|
88
|
+
|
|
89
|
+
- Never run DDL, `OPTIMIZE TABLE` or bulk writes on production without the user's explicit go-ahead.
|
|
90
|
+
- Never "fix" charset problems by changing only the column; check connection and client too.
|
|
91
|
+
- Never build SQL by concatenating user input; use placeholders.
|
|
92
|
+
- Never disable `foreign_key_checks` or `unique_checks` outside a controlled bulk load you then verify.
|
|
93
|
+
|
|
94
|
+
## Report back
|
|
95
|
+
|
|
96
|
+
For tuning: the query, the `EXPLAIN` before and after (key, type, rows, Extra), the change, and the timing difference. For design: the `CREATE TABLE`, the charset and collation chosen, and one line per index naming the query it serves.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"should_trigger": [
|
|
3
|
+
"Emoji are being saved as ???? in our MySQL database, what charset settings do I need?",
|
|
4
|
+
"EXPLAIN shows type ALL and Using filesort on this MySQL query, how should I index it?",
|
|
5
|
+
"We get Deadlock found when trying to get lock (1213) on InnoDB during concurrent inserts",
|
|
6
|
+
"Users don't see their own updates right after saving because we read from a MySQL replica",
|
|
7
|
+
"Design the InnoDB tables for a MariaDB-backed booking system, what primary keys should I use?"
|
|
8
|
+
],
|
|
9
|
+
"should_not_trigger": [
|
|
10
|
+
"Tune this PostgreSQL query, the EXPLAIN ANALYZE shows a nested loop with millions of rows",
|
|
11
|
+
"Write a Rails migration that drops a column safely across two deploys",
|
|
12
|
+
"Check this PR for N+1 queries and dangerous migrations before I merge",
|
|
13
|
+
"How do I configure PgBouncer transaction pooling for a Postgres cluster?",
|
|
14
|
+
"Set up a Redis cache with expiry for session data"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Reading MySQL EXPLAIN
|
|
2
|
+
|
|
3
|
+
## Traditional EXPLAIN columns
|
|
4
|
+
|
|
5
|
+
| Column | Meaning | Watch for |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `type` | access method, best to worst: `system`, `const`, `eq_ref`, `ref`, `range`, `index`, `ALL` | `ALL` (full table scan) or `index` (full index scan) on a large table |
|
|
8
|
+
| `possible_keys` | indexes the optimizer considered | `NULL`: no usable index for this predicate |
|
|
9
|
+
| `key` | index chosen | `NULL` while `possible_keys` is set: optimizer judged the index not selective, or stats are stale |
|
|
10
|
+
| `key_len` | bytes of the index key used | shorter than expected: only the leftmost columns are used; a range stopped the rest |
|
|
11
|
+
| `rows` | estimated rows examined | far larger than the result |
|
|
12
|
+
| `filtered` | % of rows kept after the condition | low % with high `rows`: index doesn't match the filter |
|
|
13
|
+
| `Extra` | notes | `Using filesort`, `Using temporary`, `Using where` on a scan; `Using index` is good (covering) |
|
|
14
|
+
|
|
15
|
+
## EXPLAIN ANALYZE and FORMAT=TREE
|
|
16
|
+
|
|
17
|
+
`EXPLAIN FORMAT=TREE` shows the iterator tree. `EXPLAIN ANALYZE` runs the query and adds `actual time=first..last rows=N loops=L`. Compare estimated rows with actual rows; times are per loop. MariaDB's equivalent is `ANALYZE <statement>` (and `ANALYZE FORMAT=JSON`). Check which your server supports.
|
|
18
|
+
|
|
19
|
+
## Common fixes
|
|
20
|
+
|
|
21
|
+
| Symptom | Likely cause | Fix |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `key = NULL`, `WHERE DATE(created_at) = ?` | function on column | range: `created_at >= ? AND created_at < ?` |
|
|
24
|
+
| `key = NULL`, `WHERE phone = 233201234567` on a `VARCHAR` | implicit numeric cast | pass a string |
|
|
25
|
+
| join not using index | charset or collation mismatch between columns | align both columns |
|
|
26
|
+
| `Using filesort` with `ORDER BY created_at LIMIT 20` | index doesn't end in the sort column | composite `(filter_cols..., created_at)` |
|
|
27
|
+
| `Using temporary` for `GROUP BY` | no index in group order | index on group columns, or accept for rare queries |
|
|
28
|
+
| estimate way off | stale stats | `ANALYZE TABLE t;` (cheap on InnoDB) |
|
|
29
|
+
| deep pagination slow | `OFFSET` reads and discards rows | keyset: `WHERE id > ? ORDER BY id LIMIT 20` |
|
|
30
|
+
|
|
31
|
+
## Finding slow queries
|
|
32
|
+
|
|
33
|
+
- Slow query log: `slow_query_log = ON`, `long_query_time = 1`, then summarise with `pt-query-digest`.
|
|
34
|
+
- MySQL 8 `sys.statements_with_runtimes_in_95th_percentile` and `performance_schema.events_statements_summary_by_digest` rank statements by total time.
|